expo-gaode-map-navigation 1.1.5-next.0 → 1.1.5-next.2

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 (134) 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/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +13 -3
  37. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +4 -0
  38. package/build/ExpoGaodeMapNaviView.d.ts +7 -7
  39. package/build/ExpoGaodeMapNaviView.js +10 -11
  40. package/build/index.d.ts +1 -1
  41. package/build/index.js +2 -2
  42. package/build/map/ExpoGaodeMapModule.d.ts +2 -201
  43. package/build/map/ExpoGaodeMapModule.js +586 -18
  44. package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
  45. package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
  46. package/build/map/ExpoGaodeMapView.js +66 -58
  47. package/build/map/components/FoldableMapView.d.ts +38 -0
  48. package/build/map/components/FoldableMapView.js +209 -0
  49. package/build/map/components/MapContext.d.ts +12 -0
  50. package/build/map/components/MapContext.js +54 -0
  51. package/build/map/components/MapUI.d.ts +18 -0
  52. package/build/map/components/MapUI.js +29 -0
  53. package/build/map/components/overlays/Circle.js +34 -3
  54. package/build/map/components/overlays/Cluster.d.ts +3 -1
  55. package/build/map/components/overlays/Cluster.js +31 -2
  56. package/build/map/components/overlays/HeatMap.d.ts +3 -1
  57. package/build/map/components/overlays/HeatMap.js +33 -3
  58. package/build/map/components/overlays/Marker.d.ts +1 -1
  59. package/build/map/components/overlays/Marker.js +37 -32
  60. package/build/map/components/overlays/MultiPoint.js +1 -1
  61. package/build/map/components/overlays/Polygon.js +30 -3
  62. package/build/map/components/overlays/Polyline.js +36 -3
  63. package/build/map/index.d.ts +25 -5
  64. package/build/map/index.js +59 -18
  65. package/build/map/types/common.types.d.ts +40 -0
  66. package/build/map/types/common.types.js +0 -4
  67. package/build/map/types/index.d.ts +3 -2
  68. package/build/map/types/map-view.types.d.ts +108 -3
  69. package/build/map/types/native-module.types.d.ts +363 -0
  70. package/build/map/types/native-module.types.js +5 -0
  71. package/build/map/types/offline.types.d.ts +132 -0
  72. package/build/map/types/offline.types.js +5 -0
  73. package/build/map/types/overlays.types.d.ts +137 -24
  74. package/build/map/utils/ErrorHandler.d.ts +110 -0
  75. package/build/map/utils/ErrorHandler.js +421 -0
  76. package/build/map/utils/GeoUtils.d.ts +20 -0
  77. package/build/map/utils/GeoUtils.js +76 -0
  78. package/build/map/utils/OfflineMapManager.d.ts +148 -0
  79. package/build/map/utils/OfflineMapManager.js +217 -0
  80. package/build/map/utils/PermissionUtils.d.ts +91 -0
  81. package/build/map/utils/PermissionUtils.js +255 -0
  82. package/build/map/utils/PlatformDetector.d.ts +102 -0
  83. package/build/map/utils/PlatformDetector.js +186 -0
  84. package/build/types/naviview.types.d.ts +6 -1
  85. package/expo-module.config.json +12 -10
  86. package/ios/ExpoGaodeMapNavigation.podspec +9 -0
  87. package/ios/map/ExpoGaodeMapModule.swift +485 -75
  88. package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
  89. package/ios/map/ExpoGaodeMapView.swift +611 -62
  90. package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
  91. package/ios/map/MapPreloadManager.swift +348 -0
  92. package/ios/map/cpp/ClusterEngine.cpp +110 -0
  93. package/ios/map/cpp/ClusterEngine.hpp +20 -0
  94. package/ios/map/cpp/ColorParser.cpp +135 -0
  95. package/ios/map/cpp/ColorParser.hpp +14 -0
  96. package/ios/map/cpp/GeometryEngine.cpp +574 -0
  97. package/ios/map/cpp/GeometryEngine.hpp +159 -0
  98. package/ios/map/cpp/QuadTree.cpp +92 -0
  99. package/ios/map/cpp/QuadTree.hpp +42 -0
  100. package/ios/map/cpp/README.md +55 -0
  101. package/ios/map/cpp/tests/benchmark_js.js +41 -0
  102. package/ios/map/cpp/tests/run.sh +17 -0
  103. package/ios/map/cpp/tests/test_main.cpp +276 -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 +2 -2
  129. package/build/map/ExpoGaodeMap.types.d.ts +0 -41
  130. package/build/map/ExpoGaodeMap.types.js +0 -24
  131. package/build/map/utils/EventManager.d.ts +0 -10
  132. package/build/map/utils/EventManager.js +0 -26
  133. package/build/map/utils/ModuleLoader.d.ts +0 -73
  134. package/build/map/utils/ModuleLoader.js +0 -112
@@ -0,0 +1,479 @@
1
+ import ExpoModulesCore
2
+ import AMapFoundationKit
3
+ import AMapNaviKit
4
+ import Foundation
5
+
6
+ /**
7
+ * 高德地图离线地图模块 (iOS)
8
+ *
9
+ *
10
+ */
11
+ public class ExpoGaodeMapOfflineModule: Module {
12
+
13
+ // ==================== 属性定义 ====================
14
+
15
+ // 线程安全的状态集合
16
+ private let stateQueue = DispatchQueue(label: "com.expo.gaodemap.offline.state", attributes: .concurrent)
17
+ private var _downloadingCities: Set<String> = []
18
+ private var _pausedCities: Set<String> = []
19
+
20
+ private var downloadingCities: Set<String> {
21
+ get { stateQueue.sync { _downloadingCities } }
22
+ set { stateQueue.async(flags: .barrier) { self._downloadingCities = newValue } }
23
+ }
24
+
25
+ private var pausedCities: Set<String> {
26
+ get { stateQueue.sync { _pausedCities } }
27
+ set { stateQueue.async(flags: .barrier) { self._pausedCities = newValue } }
28
+ }
29
+
30
+ private var offlineMapManager: MAOfflineMap {
31
+ return MAOfflineMap.shared()
32
+ }
33
+
34
+ // 数据缓存
35
+ private var cachedCities: [MAOfflineCity]?
36
+ private var cachedProvinces: [MAOfflineProvince]?
37
+ private var cachedMunicipalities: [MAOfflineCity]?
38
+
39
+ // 初始化锁与等待队列
40
+ private var isSetupComplete = false
41
+ private var isSetupInProgress = false
42
+ private var setupWaiters: [(Bool) -> Void] = []
43
+
44
+ // ==================== 模块定义 ====================
45
+
46
+ public func definition() -> ModuleDefinition {
47
+ Name("ExpoGaodeMapOffline")
48
+
49
+ Events(
50
+ "onDownloadProgress",
51
+ "onDownloadComplete",
52
+ "onDownloadError",
53
+ "onUnzipProgress",
54
+ "onDownloadPaused",
55
+ "onDownloadCancelled"
56
+ )
57
+
58
+ OnCreate {
59
+ // Module initialization
60
+ }
61
+
62
+ OnDestroy {
63
+ MAOfflineMap.shared().cancelAll()
64
+ self.downloadingCities.removeAll()
65
+ self.pausedCities.removeAll()
66
+ }
67
+
68
+ // ==================== 1. 地图列表管理 ====================
69
+
70
+ AsyncFunction("getAvailableCities") { (promise: Promise) in
71
+ self.ensureSetup { success in
72
+ guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
73
+ let cities = self.cachedCities ?? self.offlineMapManager.cities ?? []
74
+ promise.resolve(cities.map { self.convertCityToDict($0) })
75
+ }
76
+ }
77
+
78
+ AsyncFunction("getAvailableProvinces") { (promise: Promise) in
79
+ self.ensureSetup { success in
80
+ guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
81
+ let provinces = self.cachedProvinces ?? self.offlineMapManager.provinces ?? []
82
+ promise.resolve(provinces.map { self.convertProvinceToDict($0) })
83
+ }
84
+ }
85
+
86
+ AsyncFunction("getCitiesByProvince") { (provinceCode: String, promise: Promise) in
87
+ self.ensureSetup { success in
88
+ guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
89
+ let provinces = self.cachedProvinces ?? self.offlineMapManager.provinces ?? []
90
+ if let province = provinces.first(where: { $0.adcode == provinceCode }) {
91
+ let cities = province.cities.compactMap { ($0 as? MAOfflineCity).map { self.convertCityToDict($0) } }
92
+ promise.resolve(cities)
93
+ } else {
94
+ promise.resolve([])
95
+ }
96
+ }
97
+ }
98
+
99
+ AsyncFunction("getDownloadedMaps") { (promise: Promise) in
100
+ self.ensureSetup { success in
101
+ guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
102
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
103
+ let downloaded = allCities.filter { $0.itemStatus == .installed }
104
+ promise.resolve(downloaded.map { self.convertCityToDict($0) })
105
+ }
106
+ }
107
+
108
+ // ==================== 2. 下载管理 ====================
109
+
110
+ AsyncFunction("startDownload") { (config: [String: Any], promise: Promise) in
111
+ guard let cityCode = config["cityCode"] as? String else {
112
+ promise.reject("ERR_ARGS", "cityCode required")
113
+ return
114
+ }
115
+ self.startDownloadInternal(cityCode: cityCode, promise: promise)
116
+ }
117
+
118
+ AsyncFunction("resumeDownload") { (cityCode: String, promise: Promise) in
119
+ self.startDownloadInternal(cityCode: cityCode, promise: promise)
120
+ }
121
+
122
+ Function("pauseDownload") { (cityCode: String) in
123
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
124
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
125
+ self.pausedCities.insert(cityCode)
126
+ self.downloadingCities.remove(cityCode)
127
+ self.offlineMapManager.pause(city)
128
+ }
129
+ }
130
+
131
+ AsyncFunction("cancelDownload") { (cityCode: String) in
132
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
133
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
134
+ self.downloadingCities.remove(cityCode)
135
+ self.pausedCities.remove(cityCode)
136
+ self.offlineMapManager.pause(city) // iOS SDK 中 pause 停止网络
137
+ self.sendEvent("onDownloadCancelled", ["cityCode": cityCode, "cityName": city.name ?? ""])
138
+ }
139
+ }
140
+
141
+ AsyncFunction("deleteMap") { (cityCode: String) in
142
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
143
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
144
+ self.offlineMapManager.delete(city)
145
+ self.downloadingCities.remove(cityCode)
146
+ self.pausedCities.remove(cityCode)
147
+ }
148
+ }
149
+
150
+ AsyncFunction("updateMap") { (cityCode: String, promise: Promise) in
151
+ self.startDownloadInternal(cityCode: cityCode, promise: promise)
152
+ }
153
+
154
+ AsyncFunction("checkUpdate") { (cityCode: String, promise: Promise) in
155
+ // 检查特定城市或全局更新
156
+ self.offlineMapManager.checkNewestVersion { hasNewestVersion in
157
+ if hasNewestVersion {
158
+ // 刷新缓存以获取最新数据
159
+ self.stateQueue.async(flags: .barrier) {
160
+ self.cachedCities = nil
161
+ self.cachedProvinces = nil
162
+ }
163
+ self.parseOfflineMapData()
164
+ }
165
+ promise.resolve(hasNewestVersion)
166
+ }
167
+ }
168
+
169
+ // ==================== 3. 状态查询 ====================
170
+
171
+ AsyncFunction("isMapDownloaded") { (cityCode: String) -> Bool in
172
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
173
+ return allCities.first(where: { $0.adcode == cityCode })?.itemStatus == .installed
174
+ }
175
+
176
+ // 恢复原有方法:getMapStatus
177
+ AsyncFunction("getMapStatus") { (cityCode: String) -> [String: Any] in
178
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
179
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
180
+ return self.convertCityToDict(city)
181
+ }
182
+ return [:]
183
+ }
184
+
185
+ // 恢复原有方法:getTotalProgress (iOS SDK 无总体进度,返回 0)
186
+ AsyncFunction("getTotalProgress") { () -> Double in
187
+ return 0.0
188
+ }
189
+
190
+ AsyncFunction("getDownloadingCities") { () -> [String] in
191
+ return Array(self.downloadingCities)
192
+ }
193
+
194
+ // ==================== 4. 存储管理 ====================
195
+
196
+ AsyncFunction("getStorageSize") { () -> Int64 in
197
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
198
+ let installed = allCities.filter { $0.itemStatus == .installed }
199
+ // 修复类型转换报错:Int64(0)
200
+ return installed.reduce(Int64(0)) { $0 + ($1.downloadedSize > 0 ? $1.downloadedSize : $1.size) }
201
+ }
202
+
203
+ // 恢复原有方法:getStorageInfo
204
+ AsyncFunction("getStorageInfo") { () -> [String: Any] in
205
+ // 1. 计算离线地图占用
206
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
207
+ let installed = allCities.filter { $0.itemStatus == .installed }
208
+ let offlineMapSize = installed.reduce(Int64(0)) { $0 + ($1.downloadedSize > 0 ? $1.downloadedSize : $1.size) }
209
+
210
+ // 2. 获取系统存储信息
211
+ var totalSpace: Int64 = 0
212
+ var availableSpace: Int64 = 0
213
+ var usedSpace: Int64 = 0
214
+
215
+ do {
216
+ let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)
217
+ let values = try fileURL.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
218
+ if let total = values.volumeTotalCapacity { totalSpace = Int64(total) }
219
+ if let available = values.volumeAvailableCapacity { availableSpace = Int64(available) }
220
+ usedSpace = totalSpace - availableSpace
221
+ } catch {
222
+ // Failed to get storage info
223
+ }
224
+
225
+ return [
226
+ "totalSpace": totalSpace,
227
+ "usedSpace": usedSpace,
228
+ "availableSpace": availableSpace,
229
+ "offlineMapSize": offlineMapSize
230
+ ]
231
+ }
232
+
233
+ AsyncFunction("clearAllMaps") {
234
+ self.offlineMapManager.clearDisk()
235
+ self.downloadingCities.removeAll()
236
+ self.pausedCities.removeAll()
237
+ self.cachedCities = nil
238
+ self.parseOfflineMapData()
239
+ }
240
+
241
+ Function("setStoragePath") { (path: String) in
242
+ // iOS does not support changing storage path
243
+ }
244
+
245
+ // 恢复原有方法:getStoragePath
246
+ AsyncFunction("getStoragePath") { () -> String in
247
+ return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first ?? ""
248
+ }
249
+
250
+ // ==================== 5. 批量操作 ====================
251
+
252
+ // 恢复原有方法:batchDownload
253
+ AsyncFunction("batchDownload") { (cityCodes: [String], allowCellular: Bool?) in
254
+ // 确保初始化
255
+ self.ensureSetup { success in
256
+ guard success else { return }
257
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
258
+
259
+ cityCodes.forEach { cityCode in
260
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
261
+ self.downloadingCities.insert(cityCode)
262
+ self.pausedCities.remove(cityCode)
263
+ // 批量下载也建议开启后台
264
+ self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
265
+ guard let self = self, let item = item else { return }
266
+ self.handleDownloadCallback(item: item, status: status, info: info)
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ // 恢复原有方法:batchDelete
274
+ AsyncFunction("batchDelete") { (cityCodes: [String]) in
275
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
276
+ cityCodes.forEach { cityCode in
277
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
278
+ self.offlineMapManager.delete(city)
279
+ }
280
+ self.downloadingCities.remove(cityCode)
281
+ self.pausedCities.remove(cityCode)
282
+ }
283
+ }
284
+
285
+ // 恢复原有方法:batchUpdate
286
+ AsyncFunction("batchUpdate") { (cityCodes: [String]) in
287
+ self.ensureSetup { success in
288
+ guard success else { return }
289
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
290
+
291
+ cityCodes.forEach { cityCode in
292
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
293
+ // 只更新已下载的地图
294
+ if city.itemStatus == .installed {
295
+ self.stateQueue.async(flags: .barrier) {
296
+ self._downloadingCities.insert(cityCode)
297
+ self._pausedCities.remove(cityCode)
298
+ }
299
+ self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
300
+ guard let self = self, let item = item else { return }
301
+ self.handleDownloadCallback(item: item, status: status, info: info)
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ AsyncFunction("pauseAllDownloads") {
310
+ self.offlineMapManager.cancelAll()
311
+ for cityCode in self.downloadingCities {
312
+ self.pausedCities.insert(cityCode)
313
+ self.sendEvent("onDownloadPaused", ["cityCode": cityCode, "cityName": ""])
314
+ }
315
+ self.downloadingCities.removeAll()
316
+ }
317
+
318
+ // 恢复原有方法:resumeAllDownloads
319
+ AsyncFunction("resumeAllDownloads") {
320
+ // iOS SDK 没有 resumeAll,只能尝试恢复 pausedCities 列表中的城市
321
+ self.ensureSetup { success in
322
+ guard success else { return }
323
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
324
+ let pausedList = Array(self.pausedCities)
325
+
326
+ pausedList.forEach { cityCode in
327
+ if let city = allCities.first(where: { $0.adcode == cityCode }) {
328
+ self.pausedCities.remove(cityCode)
329
+ self.downloadingCities.insert(cityCode)
330
+ self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
331
+ guard let self = self, let item = item else { return }
332
+ self.handleDownloadCallback(item: item, status: status, info: info)
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+ // ==================== 内部私有方法 ====================
341
+
342
+ private func startDownloadInternal(cityCode: String, promise: Promise) {
343
+ self.ensureSetup { success in
344
+ guard success else {
345
+ promise.reject("ERR_SETUP", "SDK setup failed")
346
+ return
347
+ }
348
+
349
+ let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
350
+ guard let city = allCities.first(where: { $0.adcode == cityCode }) else {
351
+ promise.reject("ERR_CITY", "City not found: \(cityCode)")
352
+ return
353
+ }
354
+
355
+ self.downloadingCities.insert(cityCode)
356
+ self.pausedCities.remove(cityCode)
357
+
358
+ // 开启后台下载
359
+ self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
360
+ guard let self = self, let item = item else { return }
361
+ self.handleDownloadCallback(item: item, status: status, info: info)
362
+ }
363
+ promise.resolve(true)
364
+ }
365
+ }
366
+
367
+ private func ensureSetup(completion: @escaping (Bool) -> Void) {
368
+ if isSetupComplete { completion(true); return }
369
+ setupWaiters.append(completion)
370
+ if isSetupInProgress { return }
371
+
372
+ isSetupInProgress = true
373
+ MAOfflineMap.shared().setup { [weak self] success in
374
+ guard let self = self else { return }
375
+ self.isSetupInProgress = false
376
+ self.isSetupComplete = success
377
+ if success { self.parseOfflineMapData() }
378
+ let waiters = self.setupWaiters
379
+ self.setupWaiters.removeAll()
380
+ waiters.forEach { $0(success) }
381
+ }
382
+ }
383
+
384
+ private func parseOfflineMapData() {
385
+ let map = MAOfflineMap.shared()
386
+ stateQueue.async(flags: .barrier) {
387
+ self.cachedCities = map?.cities
388
+ self.cachedProvinces = map?.provinces
389
+ self.cachedMunicipalities = map?.municipalities
390
+ }
391
+ }
392
+
393
+ private func handleDownloadCallback(item: MAOfflineItem, status: MAOfflineMapDownloadStatus, info: Any?) {
394
+ guard let city = item as? MAOfflineCity, let cityCode = city.adcode else { return }
395
+ let cityName = city.name ?? ""
396
+
397
+ switch status {
398
+ case .progress:
399
+ if let infoDict = info as? [String: Any],
400
+ let received = infoDict[MAOfflineMapDownloadReceivedSizeKey] as? Int64,
401
+ let expected = infoDict[MAOfflineMapDownloadExpectedSizeKey] as? Int64,
402
+ expected > 0 {
403
+ let progress = Int((Double(received) / Double(expected)) * 100)
404
+ self.sendEvent("onDownloadProgress", [
405
+ "cityCode": cityCode, "cityName": cityName, "progress": progress, "receivedSize": received, "expectedSize": expected
406
+ ])
407
+ }
408
+ case .unzip:
409
+ self.sendEvent("onUnzipProgress", ["cityCode": cityCode, "cityName": cityName])
410
+ case .finished:
411
+ // 只处理 .finished 状态,避免重复事件
412
+ self.stateQueue.async(flags: .barrier) {
413
+ self._downloadingCities.remove(cityCode)
414
+ self._pausedCities.remove(cityCode)
415
+ }
416
+ self.sendEvent("onDownloadComplete", ["cityCode": cityCode, "cityName": cityName])
417
+ case .completed:
418
+ // .completed 状态只更新内部状态,不发送事件
419
+ self.stateQueue.async(flags: .barrier) {
420
+ self._downloadingCities.remove(cityCode)
421
+ self._pausedCities.remove(cityCode)
422
+ }
423
+ case .cancelled:
424
+ let wasPaused = stateQueue.sync { _pausedCities.contains(cityCode) }
425
+ self.stateQueue.async(flags: .barrier) {
426
+ self._downloadingCities.remove(cityCode)
427
+ }
428
+ if wasPaused {
429
+ self.sendEvent("onDownloadPaused", ["cityCode": cityCode, "cityName": cityName])
430
+ } else {
431
+ self.sendEvent("onDownloadCancelled", ["cityCode": cityCode, "cityName": cityName])
432
+ }
433
+ case .error:
434
+ self.stateQueue.async(flags: .barrier) {
435
+ self._downloadingCities.remove(cityCode)
436
+ }
437
+ let err = (info as? NSError)?.localizedDescription ?? "Error"
438
+ self.sendEvent("onDownloadError", ["cityCode": cityCode, "cityName": cityName, "error": err])
439
+ default: break
440
+ }
441
+ }
442
+
443
+ private func convertCityToDict(_ city: MAOfflineCity) -> [String: Any] {
444
+ var status = "not_downloaded"
445
+ switch city.itemStatus {
446
+ case .installed: status = "downloaded"
447
+ case .cached: status = "downloading"
448
+ case .expired: status = "expired"
449
+ default: status = "not_downloaded"
450
+ }
451
+
452
+ // 线程安全地检查状态
453
+ let isDownloading = stateQueue.sync { _downloadingCities.contains(city.adcode) }
454
+ let isPaused = stateQueue.sync { _pausedCities.contains(city.adcode) }
455
+
456
+ if isDownloading { status = "downloading" }
457
+ else if isPaused { status = "paused" }
458
+
459
+ return [
460
+ "cityCode": city.adcode ?? "",
461
+ "cityName": city.name ?? "",
462
+ "size": city.size,
463
+ "status": status,
464
+ "downloadedSize": city.downloadedSize,
465
+ "version": self.offlineMapManager.version ?? "",
466
+ "progress": city.size > 0 ? Int((Double(city.downloadedSize) / Double(city.size)) * 100) : 0
467
+ ]
468
+ }
469
+
470
+ private func convertProvinceToDict(_ province: MAOfflineProvince) -> [String: Any] {
471
+ return [
472
+ "cityCode": province.adcode ?? "",
473
+ "cityName": province.name ?? "",
474
+ "size": province.size,
475
+ "status": (province.itemStatus == .installed) ? "downloaded" : "not_downloaded",
476
+ "downloadedSize": province.downloadedSize
477
+ ]
478
+ }
479
+ }