expo-gaode-map 2.2.33 → 2.2.34

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 (57) hide show
  1. package/README.md +20 -14
  2. package/android/build.gradle +8 -4
  3. package/android/src/main/AndroidManifest.xml +14 -0
  4. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapModule.kt +7 -8
  5. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapOfflineModule.kt +150 -27
  6. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +24 -14
  7. package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +38 -41
  8. package/android/src/main/java/expo/modules/gaodemap/modules/SDKInitializer.kt +18 -17
  9. package/android/src/main/java/expo/modules/gaodemap/overlays/CircleView.kt +3 -1
  10. package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +6 -1
  11. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +124 -10
  12. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +2 -2
  13. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerBitmapRenderer.kt +10 -9
  14. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +7 -11
  15. package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +3 -1
  16. package/android/src/main/java/expo/modules/gaodemap/overlays/PolygonView.kt +2 -1
  17. package/android/src/main/java/expo/modules/gaodemap/overlays/PolylineView.kt +1 -0
  18. package/android/src/main/java/expo/modules/gaodemap/search/ExpoGaodeMapSearchModule.kt +751 -0
  19. package/android/src/main/java/expo/modules/gaodemap/utils/GeometryUtils.kt +5 -5
  20. package/android/src/main/java/expo/modules/gaodemap/utils/PermissionHelper.kt +13 -16
  21. package/build/ExpoGaodeMapOfflineModule.d.ts +5 -0
  22. package/build/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  23. package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
  24. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  25. package/build/components/overlays/HeatMap.js +21 -2
  26. package/build/components/overlays/HeatMap.js.map +1 -1
  27. package/build/index.d.ts +3 -0
  28. package/build/index.d.ts.map +1 -1
  29. package/build/index.js +3 -0
  30. package/build/index.js.map +1 -1
  31. package/build/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  32. package/build/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  33. package/build/search/ExpoGaodeMapSearch.types.js +19 -0
  34. package/build/search/ExpoGaodeMapSearch.types.js.map +1 -0
  35. package/build/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  36. package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  37. package/build/search/ExpoGaodeMapSearchModule.js +47 -0
  38. package/build/search/ExpoGaodeMapSearchModule.js.map +1 -0
  39. package/build/search/index.d.ts +156 -0
  40. package/build/search/index.d.ts.map +1 -0
  41. package/build/search/index.js +171 -0
  42. package/build/search/index.js.map +1 -0
  43. package/build/types/map-view.types.d.ts +4 -2
  44. package/build/types/map-view.types.d.ts.map +1 -1
  45. package/build/types/map-view.types.js.map +1 -1
  46. package/build/utils/OfflineMapManager.d.ts +4 -0
  47. package/build/utils/OfflineMapManager.d.ts.map +1 -1
  48. package/build/utils/OfflineMapManager.js +6 -0
  49. package/build/utils/OfflineMapManager.js.map +1 -1
  50. package/expo-module.config.json +4 -2
  51. package/ios/ExpoGaodeMap.podspec +2 -2
  52. package/ios/ExpoGaodeMapOfflineModule.swift +60 -0
  53. package/ios/ExpoGaodeMapSearchModule.swift +773 -0
  54. package/ios/modules/LocationManager.swift +9 -3
  55. package/ios/overlays/PolylineView.swift +6 -12
  56. package/package.json +1 -1
  57. package/plugin/build/withGaodeMap.js +12 -0
@@ -0,0 +1,773 @@
1
+ import ExpoModulesCore
2
+ import AMapSearchKit
3
+ import AMapFoundationKit
4
+
5
+ public class ExpoGaodeMapSearchModule: Module {
6
+ private var activeSearches: [String: SearchSession] = [:]
7
+
8
+ public func definition() -> ModuleDefinition {
9
+ Name("ExpoGaodeMapSearch")
10
+
11
+ // 不在 OnCreate 中初始化,改为延迟初始化
12
+
13
+ /**
14
+ * 手动初始化搜索模块(可选)
15
+ * 如果 API Key 已经设置,则直接初始化
16
+ * 如果未设置,会尝试从 Info.plist 读取
17
+ */
18
+ Function("initSearch") {
19
+ try self.ensureAPIKeyIsSet()
20
+ }
21
+
22
+ /**
23
+ * POI 搜索
24
+ */
25
+ AsyncFunction("searchPOI") { (options: [String: Any], promise: Promise) in
26
+ guard let keyword = options["keyword"] as? String else {
27
+ promise.reject("SEARCH_ERROR", "keyword is required")
28
+ return
29
+ }
30
+
31
+ let city = options["city"] as? String ?? ""
32
+ let types = options["types"] as? String ?? ""
33
+ let pageSize = options["pageSize"] as? Int ?? 20
34
+ let pageNum = options["pageNum"] as? Int ?? 1
35
+
36
+ let request = AMapPOIKeywordsSearchRequest()
37
+ request.keywords = keyword
38
+ request.city = city
39
+ request.types = types
40
+ request.page = pageNum
41
+ request.offset = pageSize
42
+ // SDK 9.4.0+ 使用 showFieldsType 控制返回字段
43
+ // AMapPOISearchShowFieldsTypeAll (返回所有扩展信息)
44
+ request.showFieldsType = AMapPOISearchShowFieldsType.all
45
+
46
+ guard let searchAPI = self.createSearchAPI(promise: promise, errorCode: "SEARCH_ERROR") else {
47
+ return
48
+ }
49
+ searchAPI.aMapPOIKeywordsSearch(request)
50
+ }
51
+
52
+ /**
53
+ * 周边搜索
54
+ */
55
+ AsyncFunction("searchNearby") { (options: [String: Any], promise: Promise) in
56
+ guard let keyword = options["keyword"] as? String else {
57
+ promise.reject("SEARCH_ERROR", "keyword is required")
58
+ return
59
+ }
60
+
61
+ guard let center = options["center"] as? [String: Any],
62
+ let latitude = center["latitude"] as? Double,
63
+ let longitude = center["longitude"] as? Double else {
64
+ promise.reject("SEARCH_ERROR", "center is required")
65
+ return
66
+ }
67
+
68
+ let radius = options["radius"] as? Int ?? 1000
69
+ let types = options["types"] as? String ?? ""
70
+ let pageSize = options["pageSize"] as? Int ?? 20
71
+ let pageNum = options["pageNum"] as? Int ?? 1
72
+
73
+ let request = AMapPOIAroundSearchRequest()
74
+ request.keywords = keyword
75
+ request.location = AMapGeoPoint.location(
76
+ withLatitude: CGFloat(latitude),
77
+ longitude: CGFloat(longitude)
78
+ )
79
+ request.radius = radius
80
+ request.types = types
81
+ request.page = pageNum
82
+ request.offset = pageSize
83
+ request.showFieldsType = AMapPOISearchShowFieldsType.all
84
+
85
+ guard let searchAPI = self.createSearchAPI(promise: promise, errorCode: "SEARCH_ERROR") else {
86
+ return
87
+ }
88
+ searchAPI.aMapPOIAroundSearch(request)
89
+ }
90
+
91
+ /**
92
+ * 沿途搜索
93
+ * iOS SDK 支持的沿途搜索类型:加油站、ATM、汽修、厕所
94
+ */
95
+ AsyncFunction("searchAlong") { (options: [String: Any], promise: Promise) in
96
+ guard let keyword = options["keyword"] as? String else {
97
+ promise.reject("SEARCH_ERROR", "keyword is required")
98
+ return
99
+ }
100
+
101
+ guard let polyline = options["polyline"] as? [[String: Any]] else {
102
+ promise.reject("SEARCH_ERROR", "polyline is required")
103
+ return
104
+ }
105
+
106
+ guard polyline.count >= 2 else {
107
+ promise.reject("SEARCH_ERROR", "polyline must have at least 2 points")
108
+ return
109
+ }
110
+
111
+ // 转换路线点
112
+ var points: [AMapGeoPoint] = []
113
+ for point in polyline {
114
+ if let lat = point["latitude"] as? Double,
115
+ let lng = point["longitude"] as? Double {
116
+ points.append(AMapGeoPoint.location(
117
+ withLatitude: CGFloat(lat),
118
+ longitude: CGFloat(lng)
119
+ ))
120
+ }
121
+ }
122
+
123
+ guard !points.isEmpty else {
124
+ promise.reject("SEARCH_ERROR", "Invalid polyline points")
125
+ return
126
+ }
127
+
128
+ let searchType: AMapRoutePOISearchType
129
+ do {
130
+ searchType = try self.resolveRoutePoiType(options: options, keyword: keyword)
131
+ } catch {
132
+ promise.reject("SEARCH_ERROR", error.localizedDescription)
133
+ return
134
+ }
135
+
136
+ let request = AMapRoutePOISearchRequest()
137
+ request.polyline = Array(points.prefix(100))
138
+ request.searchType = searchType
139
+ request.range = min(max(options["range"] as? Int ?? 250, 0), 500)
140
+ request.strategy = 0
141
+
142
+ guard let searchAPI = self.createSearchAPI(promise: promise, errorCode: "SEARCH_ERROR") else {
143
+ return
144
+ }
145
+ searchAPI.aMapRoutePOISearch(request)
146
+ }
147
+
148
+ /**
149
+ * 多边形搜索
150
+ */
151
+ AsyncFunction("searchPolygon") { (options: [String: Any], promise: Promise) in
152
+ guard let keyword = options["keyword"] as? String else {
153
+ promise.reject("SEARCH_ERROR", "keyword is required")
154
+ return
155
+ }
156
+
157
+ guard let polygon = options["polygon"] as? [[String: Any]] else {
158
+ promise.reject("SEARCH_ERROR", "polygon is required")
159
+ return
160
+ }
161
+
162
+ let types = options["types"] as? String ?? ""
163
+ let pageSize = options["pageSize"] as? Int ?? 20
164
+ let pageNum = options["pageNum"] as? Int ?? 1
165
+
166
+ // 转换路线点
167
+ var points: [AMapGeoPoint] = []
168
+ for point in polygon {
169
+ if let lat = point["latitude"] as? Double,
170
+ let lng = point["longitude"] as? Double {
171
+ points.append(AMapGeoPoint.location(
172
+ withLatitude: CGFloat(lat),
173
+ longitude: CGFloat(lng)
174
+ ))
175
+ }
176
+ }
177
+
178
+ let request = AMapPOIPolygonSearchRequest()
179
+ request.keywords = keyword
180
+ request.polygon = AMapGeoPolygon(points: points)
181
+ request.types = types
182
+ request.page = pageNum
183
+ request.offset = pageSize
184
+ request.showFieldsType = AMapPOISearchShowFieldsType.all
185
+
186
+ guard let searchAPI = self.createSearchAPI(promise: promise, errorCode: "SEARCH_ERROR") else {
187
+ return
188
+ }
189
+ searchAPI.aMapPOIPolygonSearch(request)
190
+ }
191
+
192
+ /**
193
+ * 输入提示
194
+ */
195
+ AsyncFunction("getInputTips") { (options: [String: Any], promise: Promise) in
196
+ guard let keyword = options["keyword"] as? String else {
197
+ promise.reject("TIPS_ERROR", "keyword is required")
198
+ return
199
+ }
200
+
201
+ let city = options["city"] as? String ?? ""
202
+ let types = options["types"] as? String ?? ""
203
+
204
+ let request = AMapInputTipsSearchRequest()
205
+ request.keywords = keyword
206
+ request.city = city
207
+ request.types = types
208
+ request.cityLimit = options["cityLimit"] as? Bool ?? false
209
+
210
+ guard let searchAPI = self.createSearchAPI(promise: promise, errorCode: "TIPS_ERROR") else {
211
+ return
212
+ }
213
+ searchAPI.aMapInputTipsSearch(request)
214
+ }
215
+
216
+ /**
217
+ * 逆地理编码(坐标转地址)
218
+ */
219
+ AsyncFunction("reGeocode") { (options: [String: Any], promise: Promise) in
220
+ guard let location = options["location"] as? [String: Any],
221
+ let latitude = location["latitude"] as? Double,
222
+ let longitude = location["longitude"] as? Double else {
223
+ promise.reject("SEARCH_ERROR", "location is required")
224
+ return
225
+ }
226
+
227
+ let radius = options["radius"] as? Int ?? 1000
228
+ let requireExtension = options["requireExtension"] as? Bool ?? true
229
+
230
+ let request = AMapReGeocodeSearchRequest()
231
+ request.location = AMapGeoPoint.location(withLatitude: CGFloat(latitude), longitude: CGFloat(longitude))
232
+ request.radius = limitReGeocodeRadius(radius)
233
+ request.requireExtension = requireExtension
234
+
235
+ guard let searchAPI = self.createSearchAPI(promise: promise, errorCode: "SEARCH_ERROR") else {
236
+ return
237
+ }
238
+ searchAPI.aMapReGoecodeSearch(request)
239
+ }
240
+
241
+ /**
242
+ * POI ID 搜索(详情查询)
243
+ */
244
+ AsyncFunction("getPoiDetail") { (id: String, promise: Promise) in
245
+ if id.isEmpty {
246
+ promise.reject("SEARCH_ERROR", "id is required")
247
+ return
248
+ }
249
+
250
+ let request = AMapPOIIDSearchRequest()
251
+ request.uid = id
252
+
253
+ guard let searchAPI = self.createSearchAPI(promise: promise, errorCode: "SEARCH_ERROR") else {
254
+ return
255
+ }
256
+ searchAPI.aMapPOIIDSearch(request)
257
+ }
258
+ }
259
+
260
+
261
+ // MARK: - Private Methods
262
+
263
+ private func limitReGeocodeRadius(_ radius: Int) -> Int {
264
+ // AMapReGeocodeSearchRequest radius property is NSInteger
265
+ // The documentation doesn't specify a strict limit but usually it's around 0-3000m for regeocode.
266
+ // We just cast it safely.
267
+ return radius
268
+ }
269
+
270
+ private func createSearchAPI(promise: Promise, errorCode: String) -> AMapSearchAPI? {
271
+ do {
272
+ try ensureAPIKeyIsSet()
273
+ } catch {
274
+ promise.reject(errorCode, error.localizedDescription)
275
+ return nil
276
+ }
277
+
278
+ let token = UUID().uuidString
279
+ let delegate = SearchDelegate { [weak self] in
280
+ self?.activeSearches.removeValue(forKey: token)
281
+ }
282
+ guard let searchAPI = AMapSearchAPI() else {
283
+ promise.reject(errorCode, "AMapSearchAPI 初始化失败")
284
+ return nil
285
+ }
286
+ searchAPI.delegate = delegate
287
+ delegate.currentPromise = promise
288
+ activeSearches[token] = SearchSession(api: searchAPI, delegate: delegate)
289
+ return searchAPI
290
+ }
291
+
292
+ /**
293
+ * 确保 API Key 已设置
294
+ * 优先级:已设置 > Info.plist > 提示错误
295
+ */
296
+ private func ensureAPIKeyIsSet() throws {
297
+ // 1. 检查是否已经设置(通过 initSDK 或 AppDelegate)
298
+ if let existingKey = AMapServices.shared().apiKey, !existingKey.isEmpty {
299
+ return
300
+ }
301
+
302
+ // 2. 尝试从 Info.plist 读取(Config Plugin 会写入)
303
+ if let apiKey = Bundle.main.object(forInfoDictionaryKey: "AMapApiKey") as? String, !apiKey.isEmpty {
304
+ AMapServices.shared().apiKey = apiKey
305
+ AMapServices.shared().enableHTTPS = true
306
+ return
307
+ }
308
+
309
+ throw NSError(
310
+ domain: "ExpoGaodeMapSearch",
311
+ code: 1,
312
+ userInfo: [
313
+ NSLocalizedDescriptionKey: "AMap API Key is not configured. Set iosKey in the config plugin or configure AMapApiKey in Info.plist."
314
+ ]
315
+ )
316
+ }
317
+
318
+ private func resolveRoutePoiType(options: [String: Any], keyword: String) throws -> AMapRoutePOISearchType {
319
+ if let explicitType = (options["routePoiType"] as? String) ?? (options["types"] as? String), !explicitType.isEmpty {
320
+ return try routePoiType(from: explicitType)
321
+ }
322
+
323
+ switch keyword.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
324
+ case "加油站", "加油", "gas", "gasstation", "gas_station", "gas-station":
325
+ return .gasStation
326
+ case "atm", "银行", "自动取款机":
327
+ return .ATM
328
+ case "汽修", "维修", "maintenance", "maintenancestation", "maintenance_station", "maintenance-station":
329
+ return .maintenanceStation
330
+ case "厕所", "卫生间", "toilet", "restroom", "wc":
331
+ return .toilet
332
+ case "加气站", "加气", "gasair", "gasairstation", "gas_air_station", "gas-air-station":
333
+ return .gasAirStation
334
+ case "服务区", "servicearea", "service_area", "service-area":
335
+ return .parkStation
336
+ case "充电桩", "充电站", "chargingpile", "charging_pile", "charging-pile", "chargestation", "charge_station", "charge-station":
337
+ return .chargingPile
338
+ case "美食", "餐厅", "food":
339
+ return .food
340
+ case "酒店", "宾馆", "hotel":
341
+ return .hotel
342
+ default:
343
+ throw NSError(
344
+ domain: "ExpoGaodeMapSearch",
345
+ code: 2,
346
+ userInfo: [
347
+ NSLocalizedDescriptionKey: "searchAlong only supports routePoiType: gasStation, maintenanceStation, atm, toilet, gasAirStation, serviceArea, chargingPile, food, hotel"
348
+ ]
349
+ )
350
+ }
351
+ }
352
+
353
+ private func routePoiType(from value: String) throws -> AMapRoutePOISearchType {
354
+ switch value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
355
+ case "gasstation", "gas_station", "gas-station", "gas", "加油站", "加油":
356
+ return .gasStation
357
+ case "maintenancestation", "maintenance_station", "maintenance-station", "maintenance", "汽修", "维修":
358
+ return .maintenanceStation
359
+ case "atm", "银行", "自动取款机":
360
+ return .ATM
361
+ case "toilet", "restroom", "wc", "厕所", "卫生间":
362
+ return .toilet
363
+ case "gasairstation", "gas_air_station", "gas-air-station", "fillingstation", "filling_station", "filling-station", "加气站", "加气":
364
+ return .gasAirStation
365
+ case "servicearea", "service_area", "service-area", "服务区":
366
+ return .parkStation
367
+ case "chargingpile", "charging_pile", "charging-pile", "chargestation", "charge_station", "charge-station", "充电桩", "充电站":
368
+ return .chargingPile
369
+ case "food", "美食", "餐厅":
370
+ return .food
371
+ case "hotel", "酒店", "宾馆":
372
+ return .hotel
373
+ default:
374
+ throw NSError(
375
+ domain: "ExpoGaodeMapSearch",
376
+ code: 3,
377
+ userInfo: [
378
+ NSLocalizedDescriptionKey: "Invalid routePoiType '\(value)'. Supported values: gasStation, maintenanceStation, atm, toilet, gasAirStation, serviceArea, chargingPile, food, hotel"
379
+ ]
380
+ )
381
+ }
382
+ }
383
+ }
384
+
385
+ private final class SearchSession {
386
+ let api: AMapSearchAPI
387
+ let delegate: SearchDelegate
388
+
389
+ init(api: AMapSearchAPI, delegate: SearchDelegate) {
390
+ self.api = api
391
+ self.delegate = delegate
392
+ }
393
+ }
394
+
395
+ // MARK: - Search Delegate
396
+ class SearchDelegate: NSObject, AMapSearchDelegate {
397
+ var currentPromise: Promise?
398
+ private let onComplete: () -> Void
399
+
400
+ init(onComplete: @escaping () -> Void) {
401
+ self.onComplete = onComplete
402
+ super.init()
403
+ }
404
+
405
+ private func finish(_ block: (Promise) -> Void) {
406
+ guard let promise = currentPromise else {
407
+ onComplete()
408
+ return
409
+ }
410
+
411
+ currentPromise = nil
412
+ block(promise)
413
+ onComplete()
414
+ }
415
+
416
+ /**
417
+ * POI 搜索回调
418
+ */
419
+ func onPOISearchDone(_ request: AMapPOISearchBaseRequest!, response: AMapPOISearchResponse!) {
420
+ finish { promise in
421
+ if let response = response {
422
+ promise.resolve(convertPOISearchResponse(response, request: request))
423
+ } else {
424
+ promise.reject("SEARCH_ERROR", "Search failed")
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * 沿途搜索回调
431
+ */
432
+ func onRoutePOISearchDone(_ request: AMapRoutePOISearchRequest!, response: AMapRoutePOISearchResponse!) {
433
+ finish { promise in
434
+ if let response = response {
435
+ promise.resolve(convertRoutePOISearchResponse(response))
436
+ } else {
437
+ promise.reject("SEARCH_ERROR", "Route search failed")
438
+ }
439
+ }
440
+ }
441
+
442
+ /**
443
+ * 输入提示回调
444
+ */
445
+ func onInputTipsSearchDone(_ request: AMapInputTipsSearchRequest!, response: AMapInputTipsSearchResponse!) {
446
+ finish { promise in
447
+ if let response = response {
448
+ promise.resolve(convertInputTipsResponse(response))
449
+ } else {
450
+ promise.reject("TIPS_ERROR", "Input tips failed")
451
+ }
452
+ }
453
+ }
454
+
455
+ /**
456
+ * 逆地理编码回调
457
+ */
458
+ func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
459
+ finish { promise in
460
+ if let response = response {
461
+ promise.resolve(convertReGeocodeResponse(response))
462
+ } else {
463
+ promise.reject("SEARCH_ERROR", "ReGeocode failed")
464
+ }
465
+ }
466
+ }
467
+
468
+ /**
469
+ * 搜索失败回调
470
+ */
471
+ func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
472
+ finish { promise in
473
+ let errorMessage = error?.localizedDescription ?? "Unknown error"
474
+ promise.reject("SEARCH_ERROR", errorMessage)
475
+ }
476
+ }
477
+
478
+ // MARK: - 转换方法
479
+
480
+ /**
481
+ * 转换 POI 搜索结果
482
+ */
483
+ private func convertPOISearchResponse(_ response: AMapPOISearchResponse, request: AMapPOISearchBaseRequest!) -> Any {
484
+ // 检查是否是 ID 搜索(只有 1 个结果且不是列表)
485
+ // 但 AMapPOISearchResponse 总是返回列表
486
+ // 我们约定如果是 ID 搜索,我们在 JS 层处理,这里依然返回列表格式,或者如果是单个 POI,我们取第一个
487
+
488
+ // 注意:ID 搜索返回的也是 AMapPOISearchResponse
489
+
490
+ let pois = response.pois?.map { poi -> [String: Any] in
491
+ return convertPOI(poi)
492
+ } ?? []
493
+
494
+ // 如果是 ID 搜索,通常 pois 只有一个
495
+ // 为了保持 searchPOI 接口返回结构一致,我们总是返回列表结构
496
+ // 但对于 getPoiDetail,我们需要单个对象。
497
+ // 由于 native 无法区分当前是 searchPOI 还是 getPoiDetail 调用的回调(除非用不同 delegate 方法,但 SDK 都是 onPOISearchDone)
498
+ // 我们可以根据 request 类型判断?SDK 回调中 request 参数是基类 AMapPOISearchBaseRequest
499
+
500
+ if request is AMapPOIIDSearchRequest {
501
+ if let first = pois.first {
502
+ return first
503
+ } else {
504
+ return [:] // 没找到
505
+ }
506
+ }
507
+
508
+ return [
509
+ "pois": pois,
510
+ "total": response.count,
511
+ "pageNum": max(request.page, 1),
512
+ "pageSize": request.offset,
513
+ "pageCount": request.offset > 0 ? (response.count + request.offset - 1) / request.offset : 0
514
+ ]
515
+ }
516
+
517
+ private func convertPOI(_ poi: AMapPOI) -> [String: Any] {
518
+ var result: [String: Any] = [
519
+ "id": poi.uid ?? "",
520
+ "name": poi.name ?? "",
521
+ "address": poi.address ?? "",
522
+ "location": [
523
+ "latitude": poi.location?.latitude ?? 0,
524
+ "longitude": poi.location?.longitude ?? 0
525
+ ],
526
+ "typeCode": poi.typecode ?? "",
527
+ "typeDes": poi.type ?? "",
528
+ "tel": poi.tel ?? "",
529
+ "distance": poi.distance,
530
+ "cityName": poi.city ?? "",
531
+ "cityCode": poi.citycode ?? "",
532
+ "provinceName": poi.province ?? "",
533
+ "adName": poi.district ?? "",
534
+ "adCode": poi.adcode ?? "",
535
+ "businessArea": poi.businessArea ?? "",
536
+ "parkingType": poi.parkingType ?? "",
537
+ "website": poi.website ?? "",
538
+ "email": poi.email ?? "",
539
+ "postcode": poi.postcode ?? ""
540
+ ]
541
+
542
+ // 图片信息
543
+ if let images = poi.images {
544
+ result["photos"] = images.map { image in
545
+ return [
546
+ "title": image.title ?? "",
547
+ "url": image.url ?? ""
548
+ ]
549
+ }
550
+ }
551
+
552
+ // 室内信息
553
+ if let indoor = poi.indoorData {
554
+ result["indoor"] = [
555
+ "floor": indoor.floor,
556
+ "floorName": indoor.floorName ?? "",
557
+ "poiId": indoor.pid ?? "",
558
+ "hasIndoorMap": poi.hasIndoorMap
559
+ ]
560
+ }
561
+
562
+ // 深度信息 (Business)
563
+ // 只有当有扩展信息或特定业务字段时才返回
564
+ var business: [String: Any] = [
565
+ "tel": poi.tel ?? "",
566
+ "parkingType": poi.parkingType ?? "",
567
+ "businessArea": poi.businessArea ?? ""
568
+ ]
569
+
570
+ // 合并 extensionInfo
571
+ if let ext = poi.extensionInfo {
572
+ business["rating"] = String(format: "%.1f", ext.rating)
573
+ business["cost"] = String(format: "%.1f", ext.cost)
574
+ business["opentime"] = ext.openTime ?? ""
575
+ }
576
+
577
+ // 合并 businessData (SDK 9.4.0 新增)
578
+ if let biz = poi.businessData {
579
+ business["alias"] = biz.alias ?? ""
580
+ business["tag"] = biz.tag ?? ""
581
+
582
+ // 如果 extensionInfo 没有这些字段,优先使用 businessData
583
+ if business["rating"] == nil || (business["rating"] as? String) == "0.0" {
584
+ business["rating"] = biz.rating ?? ""
585
+ }
586
+ if business["cost"] == nil || (business["cost"] as? String) == "0.0" {
587
+ business["cost"] = biz.cost ?? ""
588
+ }
589
+ if business["opentime"] == nil || (business["opentime"] as? String) == "" {
590
+ business["opentime"] = biz.opentimeWeek ?? ""
591
+ }
592
+
593
+ business["opentimeToday"] = biz.opentimeToday ?? ""
594
+
595
+ // 如果外层没有 tel/parkingType/businessArea,尝试从 businessData 获取
596
+ if (business["tel"] as? String) == "" { business["tel"] = biz.tel ?? "" }
597
+ if (business["parkingType"] as? String) == "" { business["parkingType"] = biz.parkingType ?? "" }
598
+ if (business["businessArea"] as? String) == "" { business["businessArea"] = biz.businessArea ?? "" }
599
+ }
600
+
601
+ result["business"] = business
602
+
603
+ return result
604
+ }
605
+
606
+ /**
607
+ * 转换输入提示结果
608
+ */
609
+ private func convertInputTipsResponse(_ response: AMapInputTipsSearchResponse) -> [String: Any] {
610
+ let tips = response.tips?.map { tip -> [String: Any] in
611
+ var result: [String: Any] = [
612
+ "id": tip.uid ?? "",
613
+ "name": tip.name ?? "",
614
+ "address": tip.address ?? "",
615
+ "typeCode": tip.typecode ?? "",
616
+ "cityName": tip.district ?? "",
617
+ "adName": tip.district ?? ""
618
+ ]
619
+
620
+ if let location = tip.location {
621
+ result["location"] = [
622
+ "latitude": location.latitude,
623
+ "longitude": location.longitude
624
+ ]
625
+ }
626
+
627
+ return result
628
+ } ?? []
629
+
630
+ return ["tips": tips]
631
+ }
632
+
633
+ /**
634
+ * 转换逆地理编码结果
635
+ */
636
+ private func convertReGeocodeResponse(_ response: AMapReGeocodeSearchResponse) -> [String: Any] {
637
+ guard let regeocode = response.regeocode else {
638
+ return [:]
639
+ }
640
+
641
+ var result: [String: Any] = [
642
+ "formattedAddress": regeocode.formattedAddress ?? ""
643
+ ]
644
+
645
+ if let addressComponent = regeocode.addressComponent {
646
+ let streetNumber = addressComponent.streetNumber
647
+ let streetNumberDict: [String: Any] = [
648
+ "street": streetNumber?.street ?? "",
649
+ "number": streetNumber?.number ?? "",
650
+ "direction": streetNumber?.direction ?? "",
651
+ "distance": streetNumber?.distance ?? 0
652
+ ]
653
+
654
+ let businessAreas = addressComponent.businessAreas?.map { area -> [String: Any] in
655
+ return [
656
+ "name": area.name ?? "",
657
+ "location": [
658
+ "latitude": area.location?.latitude ?? 0,
659
+ "longitude": area.location?.longitude ?? 0
660
+ ]
661
+ ]
662
+ } ?? []
663
+
664
+ result["addressComponent"] = [
665
+ "province": addressComponent.province ?? "",
666
+ "city": addressComponent.city ?? "",
667
+ "district": addressComponent.district ?? "",
668
+ "township": addressComponent.township ?? "",
669
+ "neighborhood": addressComponent.neighborhood ?? "",
670
+ "building": addressComponent.building ?? "",
671
+ "cityCode": addressComponent.citycode ?? "",
672
+ "adCode": addressComponent.adcode ?? "",
673
+ "streetNumber": streetNumberDict,
674
+ "businessAreas": businessAreas
675
+ ]
676
+ }
677
+
678
+ if let pois = regeocode.pois {
679
+ result["pois"] = pois.map { poi -> [String: Any] in
680
+ return [
681
+ "id": poi.uid ?? "",
682
+ "name": poi.name ?? "",
683
+ "typeCode": poi.typecode ?? "",
684
+ "typeDes": poi.type ?? "",
685
+ "tel": poi.tel ?? "",
686
+ "distance": poi.distance,
687
+ "direction": poi.direction ?? "",
688
+ "address": poi.address ?? "",
689
+ "location": [
690
+ "latitude": poi.location?.latitude ?? 0,
691
+ "longitude": poi.location?.longitude ?? 0
692
+ ]
693
+ ]
694
+ }
695
+ }
696
+
697
+ if let aois = regeocode.aois {
698
+ result["aois"] = aois.map { aoi -> [String: Any] in
699
+ return [
700
+ "id": aoi.uid ?? "",
701
+ "name": aoi.name ?? "",
702
+ "adCode": aoi.adcode ?? "",
703
+ "location": [
704
+ "latitude": aoi.location?.latitude ?? 0,
705
+ "longitude": aoi.location?.longitude ?? 0
706
+ ],
707
+ "area": aoi.area
708
+ ]
709
+ }
710
+ }
711
+
712
+ if let roads = regeocode.roads {
713
+ result["roads"] = roads.map { road -> [String: Any] in
714
+ return [
715
+ "id": road.uid ?? "",
716
+ "name": road.name ?? "",
717
+ "distance": road.distance,
718
+ "direction": road.direction ?? "",
719
+ "location": [
720
+ "latitude": road.location?.latitude ?? 0,
721
+ "longitude": road.location?.longitude ?? 0
722
+ ]
723
+ ]
724
+ }
725
+ }
726
+
727
+ if let roadinters = regeocode.roadinters {
728
+ result["roadCrosses"] = roadinters.map { cross -> [String: Any] in
729
+ return [
730
+ "distance": cross.distance,
731
+ "direction": cross.direction ?? "",
732
+ "location": [
733
+ "latitude": cross.location?.latitude ?? 0,
734
+ "longitude": cross.location?.longitude ?? 0
735
+ ],
736
+ "firstId": cross.firstId ?? "",
737
+ "firstName": cross.firstName ?? "",
738
+ "secondId": cross.secondId ?? "",
739
+ "secondName": cross.secondName ?? ""
740
+ ]
741
+ }
742
+ }
743
+
744
+ return result
745
+ }
746
+
747
+ /**
748
+ * 转换沿途 POI 搜索结果
749
+ */
750
+ private func convertRoutePOISearchResponse(_ response: AMapRoutePOISearchResponse) -> [String: Any] {
751
+ let pois = response.pois?.map { poi -> [String: Any] in
752
+ let result: [String: Any] = [
753
+ "id": poi.uid ?? "",
754
+ "name": poi.name ?? "",
755
+ "address": "",
756
+ "location": [
757
+ "latitude": poi.location?.latitude ?? 0,
758
+ "longitude": poi.location?.longitude ?? 0
759
+ ],
760
+ "distance": poi.distance
761
+ ]
762
+ return result
763
+ } ?? []
764
+
765
+ return [
766
+ "pois": pois,
767
+ "total": pois.count,
768
+ "pageNum": 1,
769
+ "pageSize": pois.count,
770
+ "pageCount": 1
771
+ ]
772
+ }
773
+ }