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,751 @@
1
+ package expo.modules.gaodemap.search
2
+
3
+ import android.content.Context
4
+
5
+ import com.amap.api.services.core.LatLonPoint
6
+ import com.amap.api.services.core.PoiItem
7
+ import com.amap.api.services.core.PoiItemV2
8
+ import com.amap.api.services.core.ServiceSettings
9
+
10
+ import com.amap.api.services.geocoder.GeocodeResult
11
+ import com.amap.api.services.geocoder.GeocodeSearch
12
+ import com.amap.api.services.geocoder.RegeocodeQuery
13
+ import com.amap.api.services.geocoder.RegeocodeResult
14
+ import com.amap.api.services.help.Inputtips
15
+ import com.amap.api.services.help.InputtipsQuery
16
+ import com.amap.api.services.help.Tip
17
+ import com.amap.api.services.poisearch.PoiResultV2
18
+ import com.amap.api.services.poisearch.PoiSearchV2
19
+
20
+ import com.amap.api.services.routepoisearch.RoutePOISearch
21
+ import com.amap.api.services.routepoisearch.RoutePOISearchQuery
22
+ import com.amap.api.services.routepoisearch.RoutePOISearchResult
23
+ import expo.modules.kotlin.Promise
24
+ import expo.modules.kotlin.modules.Module
25
+ import expo.modules.kotlin.modules.ModuleDefinition
26
+ import kotlin.collections.mapOf
27
+
28
+ class ExpoGaodeMapSearchModule : Module() {
29
+ private val context: Context
30
+ get() = appContext.reactContext ?: throw Exception("React context is null")
31
+
32
+ override fun definition() = ModuleDefinition {
33
+ Name("ExpoGaodeMapSearch")
34
+
35
+ /**
36
+ * 手动初始化搜索模块(可选)
37
+ */
38
+ Function("initSearch") {
39
+ ensureAPIKeyIsSet()
40
+ }
41
+
42
+ /**
43
+ * POI 搜索
44
+ */
45
+ AsyncFunction("searchPOI") { options: Map<String, Any?>, promise: Promise ->
46
+ try {
47
+ ensureAPIKeyIsSet()
48
+ val keyword = options["keyword"] as? String
49
+ ?: throw Exception("keyword is required")
50
+
51
+ val city = options["city"] as? String ?: ""
52
+ val types = options["types"] as? String ?: ""
53
+ val pageSize = (options["pageSize"] as? Number)?.toInt() ?: 20
54
+ val pageNum = (options["pageNum"] as? Number)?.toInt() ?: 1
55
+
56
+ val query = PoiSearchV2.Query(keyword, types, city)
57
+ query.pageSize = pageSize
58
+ query.pageNum = pageNum - 1 // 高德 SDK 从 0 开始
59
+ query.showFields = PoiSearchV2.ShowFields(PoiSearchV2.ShowFields.ALL)
60
+
61
+ val poiSearch = PoiSearchV2(context, query)
62
+
63
+ poiSearch.setOnPoiSearchListener(object : PoiSearchV2.OnPoiSearchListener {
64
+ override fun onPoiSearched(result: PoiResultV2?, rCode: Int) {
65
+ if (rCode == 1000) {
66
+ promise.resolve(convertPoiResult(result))
67
+ } else {
68
+ promise.reject("SEARCH_ERROR", "Search failed with code: $rCode", null)
69
+ }
70
+ }
71
+
72
+ override fun onPoiItemSearched(item: PoiItemV2?, rCode: Int) {
73
+ // 不使用单个 POI 搜索
74
+ }
75
+ })
76
+
77
+ poiSearch.searchPOIAsyn()
78
+ } catch (e: Exception) {
79
+ promise.reject("SEARCH_ERROR", e.message, e)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 周边搜索
85
+ */
86
+ AsyncFunction("searchNearby") { options: Map<String, Any?>, promise: Promise ->
87
+ try {
88
+ ensureAPIKeyIsSet()
89
+ val keyword = options["keyword"] as? String
90
+ ?: throw Exception("keyword is required")
91
+
92
+ @Suppress("UNCHECKED_CAST")
93
+ val center = options["center"] as? Map<String, Any?>
94
+ ?: throw Exception("center is required")
95
+
96
+ val latitude = (center["latitude"] as? Number)?.toDouble()
97
+ ?: throw Exception("center.latitude is required")
98
+ val longitude = (center["longitude"] as? Number)?.toDouble()
99
+ ?: throw Exception("center.longitude is required")
100
+
101
+ val radius = (options["radius"] as? Number)?.toInt() ?: 1000
102
+ val types = options["types"] as? String ?: ""
103
+ val pageSize = (options["pageSize"] as? Number)?.toInt() ?: 20
104
+ val pageNum = (options["pageNum"] as? Number)?.toInt() ?: 1
105
+
106
+ val query = PoiSearchV2.Query(keyword, types)
107
+ query.pageSize = pageSize
108
+ query.pageNum = pageNum - 1
109
+ query.showFields = PoiSearchV2.ShowFields(PoiSearchV2.ShowFields.ALL)
110
+
111
+ val poiSearch = PoiSearchV2(context, query)
112
+ val centerPoint = LatLonPoint(latitude, longitude)
113
+ poiSearch.bound = PoiSearchV2.SearchBound(
114
+ centerPoint, radius
115
+ )
116
+
117
+ poiSearch.setOnPoiSearchListener(object : PoiSearchV2.OnPoiSearchListener {
118
+ override fun onPoiSearched(result: PoiResultV2?, rCode: Int) {
119
+ if (rCode == 1000) {
120
+ promise.resolve(convertPoiResult(result, centerPoint))
121
+ } else {
122
+ promise.reject("SEARCH_ERROR", "Search failed with code: $rCode", null)
123
+ }
124
+ }
125
+
126
+ override fun onPoiItemSearched(item: PoiItemV2?, rCode: Int) {}
127
+ })
128
+
129
+ poiSearch.searchPOIAsyn()
130
+ } catch (e: Exception) {
131
+ promise.reject("SEARCH_ERROR", e.message, e)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * 沿途搜索
137
+ */
138
+ AsyncFunction("searchAlong") { options: Map<String, Any?>, promise: Promise ->
139
+ try {
140
+ ensureAPIKeyIsSet()
141
+ val keyword = options["keyword"] as? String
142
+ ?: throw Exception("keyword is required")
143
+
144
+ @Suppress("UNCHECKED_CAST")
145
+ val polyline = options["polyline"] as? List<Map<String, Any?>>
146
+ ?: throw Exception("polyline is required")
147
+
148
+ if (polyline.size < 2) {
149
+ throw Exception("polyline must have at least 2 points")
150
+ }
151
+
152
+ // 转换路线点
153
+ val points = polyline.map { point ->
154
+ val lat = (point["latitude"] as? Number)?.toDouble()
155
+ ?: throw Exception("Invalid polyline point")
156
+ val lng = (point["longitude"] as? Number)?.toDouble()
157
+ ?: throw Exception("Invalid polyline point")
158
+ LatLonPoint(lat, lng)
159
+ }
160
+
161
+ val searchRange = ((options["range"] as? Number)?.toInt() ?: 250).coerceIn(0, 500)
162
+ val searchType = resolveRoutePoiType(options, keyword)
163
+ val query = RoutePOISearchQuery(points.take(100), searchType, searchRange)
164
+
165
+ val routePOISearch = RoutePOISearch(context, query)
166
+
167
+ routePOISearch.setPoiSearchListener { result, rCode ->
168
+ if (rCode == 1000 && result != null) {
169
+ promise.resolve(convertRoutePOIResult(result))
170
+ } else {
171
+ promise.reject("SEARCH_ERROR", "Route search failed with code: $rCode", null)
172
+ }
173
+ }
174
+
175
+ routePOISearch.searchRoutePOIAsyn()
176
+ } catch (e: Exception) {
177
+ promise.reject("SEARCH_ERROR", e.message, e)
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 多边形搜索(使用矩形范围代替)
183
+ */
184
+ AsyncFunction("searchPolygon") { options: Map<String, Any?>, promise: Promise ->
185
+ try {
186
+ ensureAPIKeyIsSet()
187
+ val keyword = options["keyword"] as? String
188
+ ?: throw Exception("keyword is required")
189
+ @Suppress("UNCHECKED_CAST")
190
+ val polygon = options["polygon"] as? List<Map<String, Any?>>
191
+ ?: throw Exception("polygon is required")
192
+
193
+ val types = options["types"] as? String ?: ""
194
+ val pageSize = (options["pageSize"] as? Number)?.toInt() ?: 20
195
+ val pageNum = (options["pageNum"] as? Number)?.toInt() ?: 1
196
+
197
+ // 计算边界矩形
198
+ val points = polygon.map { point ->
199
+ val lat = (point["latitude"] as? Number)?.toDouble()
200
+ ?: throw Exception("Invalid polygon point")
201
+ val lng = (point["longitude"] as? Number)?.toDouble()
202
+ ?: throw Exception("Invalid polygon point")
203
+ LatLonPoint(lat, lng)
204
+ }
205
+
206
+
207
+
208
+ val query = PoiSearchV2.Query(keyword, types)
209
+ // 策略调整:为了解决 SDK 多边形搜索实为矩形搜索导致过滤后数据量过少的问题
210
+ // 我们强制请求最大数量 (50),然后在客户端过滤
211
+ // 注意:这会破坏精确的分页对应关系,但在多边形搜索场景下,优先保证返回有效数据更重要
212
+ val sdkPageSize = 50
213
+ query.pageSize = sdkPageSize
214
+ query.pageNum = pageNum - 1
215
+ query.showFields = PoiSearchV2.ShowFields(PoiSearchV2.ShowFields.ALL)
216
+
217
+ val poiSearch = PoiSearchV2(context, query)
218
+
219
+ // 使用 SDK 原生多边形搜索
220
+ poiSearch.bound = PoiSearchV2.SearchBound(points)
221
+
222
+ poiSearch.setOnPoiSearchListener(object : PoiSearchV2.OnPoiSearchListener {
223
+ override fun onPoiSearched(result: PoiResultV2?, rCode: Int) {
224
+ if (rCode == 1000) {
225
+ val convertedResult = convertPoiResult(result).toMutableMap()
226
+ val pois = convertedResult["pois"] as? List<Map<String, Any?>>
227
+
228
+ if (pois != null) {
229
+ // 1. 客户端过滤:只保留在多边形内的点
230
+ val filteredPois = pois.filter { poi ->
231
+ val location = poi["location"] as? Map<String, Any?>
232
+ val lat = (location?.get("latitude") as? Number)?.toDouble()
233
+ val lng = (location?.get("longitude") as? Number)?.toDouble()
234
+
235
+ if (lat != null && lng != null) {
236
+ isPointInPolygon(LatLonPoint(lat, lng), points)
237
+ } else {
238
+ false
239
+ }
240
+ }
241
+
242
+ // 2. 处理分页截断 (恢复用户请求的 pageSize)
243
+ // 如果过滤后的数量多于用户请求的 pageSize,进行截断
244
+ // 注意:由于我们在 SDK 层请求了 50 条,这里尽量返回给用户填满 pageSize 的数据
245
+ val userPageSize = pageSize
246
+ val finalPois = if (filteredPois.size > userPageSize) {
247
+ filteredPois.subList(0, userPageSize)
248
+ } else {
249
+ filteredPois
250
+ }
251
+
252
+ convertedResult["pois"] = finalPois
253
+ // 更新统计信息,反映过滤后的实际情况
254
+ convertedResult["pageSize"] = userPageSize
255
+ // total 仍然是 SDK 返回的矩形区域总数,无法精确修正,保持原样或不返回
256
+ }
257
+
258
+ promise.resolve(convertedResult)
259
+ } else {
260
+ promise.reject("SEARCH_ERROR", "Search failed with code: $rCode", null)
261
+ }
262
+ }
263
+
264
+ override fun onPoiItemSearched(item: PoiItemV2?, rCode: Int) {}
265
+ })
266
+
267
+ poiSearch.searchPOIAsyn()
268
+ } catch (e: Exception) {
269
+ promise.reject("SEARCH_ERROR", e.message, e)
270
+ }
271
+ }
272
+
273
+ /**
274
+ * 输入提示
275
+ */
276
+ AsyncFunction("getInputTips") { options: Map<String, Any?>, promise: Promise ->
277
+ try {
278
+ ensureAPIKeyIsSet()
279
+ val keyword = options["keyword"] as? String
280
+ ?: throw Exception("keyword is required")
281
+
282
+ val city = options["city"] as? String ?: ""
283
+ val types = options["types"] as? String ?: ""
284
+
285
+ val query = InputtipsQuery(keyword, city)
286
+ if (types.isNotEmpty()) {
287
+ query.setType(types)
288
+ }
289
+ query.cityLimit = options["cityLimit"] as? Boolean ?: false
290
+
291
+ val inputtips = Inputtips(context, query)
292
+
293
+ inputtips.setInputtipsListener { tipList, rCode ->
294
+ if (rCode == 1000) {
295
+ promise.resolve(convertTipsResult(tipList))
296
+ } else {
297
+ promise.reject("TIPS_ERROR", "Input tips failed with code: $rCode", null)
298
+ }
299
+ }
300
+
301
+ inputtips.requestInputtipsAsyn()
302
+ } catch (e: Exception) {
303
+ promise.reject("TIPS_ERROR", e.message, e)
304
+ }
305
+ }
306
+
307
+ /**
308
+ * 逆地理编码(坐标转地址)
309
+ */
310
+ AsyncFunction("reGeocode") { options: Map<String, Any?>, promise: Promise ->
311
+ try {
312
+ ensureAPIKeyIsSet()
313
+ @Suppress("UNCHECKED_CAST")
314
+ val location = options["location"] as? Map<String, Any?>
315
+ ?: throw Exception("location is required")
316
+
317
+ val latitude = (location["latitude"] as? Number)?.toDouble()
318
+ ?: throw Exception("location.latitude is required")
319
+ val longitude = (location["longitude"] as? Number)?.toDouble()
320
+ ?: throw Exception("location.longitude is required")
321
+
322
+ val radius = (options["radius"] as? Number)?.toFloat() ?: 1000f
323
+
324
+ val geocodeSearch = GeocodeSearch(context)
325
+ geocodeSearch.setOnGeocodeSearchListener(object : GeocodeSearch.OnGeocodeSearchListener {
326
+ override fun onRegeocodeSearched(result: RegeocodeResult?, rCode: Int) {
327
+ if (rCode == 1000 && result != null) {
328
+ promise.resolve(convertRegeocodeResult(result))
329
+ } else {
330
+ promise.reject("SEARCH_ERROR", "ReGeocode failed with code: $rCode", null)
331
+ }
332
+ }
333
+
334
+ override fun onGeocodeSearched(result: GeocodeResult?, rCode: Int) {
335
+ // Not used
336
+ }
337
+ })
338
+
339
+ val point = LatLonPoint(latitude, longitude)
340
+ val query = RegeocodeQuery(point, radius, GeocodeSearch.AMAP)
341
+ query.extensions = "all"
342
+
343
+ geocodeSearch.getFromLocationAsyn(query)
344
+ } catch (e: Exception) {
345
+ promise.reject("SEARCH_ERROR", e.message, e)
346
+ }
347
+ }
348
+
349
+ /**
350
+ * POI ID 搜索(详情查询)
351
+ */
352
+ AsyncFunction("getPoiDetail") { id: String, promise: Promise ->
353
+ try {
354
+ ensureAPIKeyIsSet()
355
+ if (id.isEmpty()) {
356
+ throw Exception("id is required")
357
+ }
358
+
359
+ val poiSearch = PoiSearchV2(context, null)
360
+ poiSearch.setOnPoiSearchListener(object : PoiSearchV2.OnPoiSearchListener {
361
+ override fun onPoiSearched(result: PoiResultV2?, rCode: Int) {
362
+ // Not used
363
+ }
364
+
365
+ override fun onPoiItemSearched(item: PoiItemV2?, rCode: Int) {
366
+ if (rCode == 1000 && item != null) {
367
+ promise.resolve(convertPoiItem(item))
368
+ } else {
369
+ promise.reject("SEARCH_ERROR", "Get POI detail failed with code: $rCode", null)
370
+ }
371
+ }
372
+ })
373
+
374
+ poiSearch.searchPOIIdAsyn(id)
375
+ } catch (e: Exception) {
376
+ promise.reject("SEARCH_ERROR", e.message, e)
377
+ }
378
+ }
379
+ }
380
+
381
+ // MARK: - Private Methods
382
+
383
+ /**
384
+ * 判断点是否在多边形内 (Ray Casting 算法)
385
+ */
386
+ private fun isPointInPolygon(point: LatLonPoint, vertices: List<LatLonPoint>): Boolean {
387
+ var inside = false
388
+ var j = vertices.size - 1
389
+ for (i in vertices.indices) {
390
+ if (((vertices[i].latitude > point.latitude) != (vertices[j].latitude > point.latitude)) &&
391
+ (point.longitude < (vertices[j].longitude - vertices[i].longitude) * (point.latitude - vertices[i].latitude) / (vertices[j].latitude - vertices[i].latitude) + vertices[i].longitude)
392
+ ) {
393
+ inside = !inside
394
+ }
395
+ j = i
396
+ }
397
+ return inside
398
+ }
399
+
400
+ private fun resolveRoutePoiType(
401
+ options: Map<String, Any?>,
402
+ keyword: String
403
+ ): RoutePOISearch.RoutePOISearchType {
404
+ val explicitType = (options["routePoiType"] as? String)
405
+ ?: (options["types"] as? String)?.takeIf { it.isNotBlank() }
406
+
407
+ explicitType?.let { return routePoiTypeFromString(it) }
408
+
409
+ return when (keyword.trim().lowercase()) {
410
+ "加油站", "加油", "gas", "gasstation", "gas_station", "gas-station" ->
411
+ RoutePOISearch.RoutePOISearchType.TypeGasStation
412
+ "atm", "银行", "自动取款机" ->
413
+ RoutePOISearch.RoutePOISearchType.TypeATM
414
+ "汽修", "维修", "maintenance", "maintenancestation", "maintenance_station", "maintenance-station" ->
415
+ RoutePOISearch.RoutePOISearchType.TypeMaintenanceStation
416
+ "厕所", "卫生间", "toilet", "restroom", "wc" ->
417
+ RoutePOISearch.RoutePOISearchType.TypeToilet
418
+ "加气站", "加气", "gasair", "gasairstation", "gas_air_station", "gas-air-station" ->
419
+ RoutePOISearch.RoutePOISearchType.TypeFillingStation
420
+ "服务区", "servicearea", "service_area", "service-area" ->
421
+ RoutePOISearch.RoutePOISearchType.TypeServiceArea
422
+ "充电桩", "充电站", "chargingpile", "charging_pile", "charging-pile", "chargestation", "charge_station", "charge-station" ->
423
+ RoutePOISearch.RoutePOISearchType.TypeChargeStation
424
+ "美食", "餐厅", "food" ->
425
+ RoutePOISearch.RoutePOISearchType.TypeFood
426
+ "酒店", "宾馆", "hotel" ->
427
+ RoutePOISearch.RoutePOISearchType.TypeHotel
428
+ else -> throw IllegalArgumentException(
429
+ "searchAlong only supports routePoiType: gasStation, maintenanceStation, atm, toilet, gasAirStation, serviceArea, chargingPile, food, hotel"
430
+ )
431
+ }
432
+ }
433
+
434
+ private fun routePoiTypeFromString(value: String): RoutePOISearch.RoutePOISearchType {
435
+ return when (value.trim().lowercase()) {
436
+ "gasstation", "gas_station", "gas-station", "gas", "加油站", "加油" ->
437
+ RoutePOISearch.RoutePOISearchType.TypeGasStation
438
+ "maintenancestation", "maintenance_station", "maintenance-station", "maintenance", "汽修", "维修" ->
439
+ RoutePOISearch.RoutePOISearchType.TypeMaintenanceStation
440
+ "atm", "银行", "自动取款机" ->
441
+ RoutePOISearch.RoutePOISearchType.TypeATM
442
+ "toilet", "restroom", "wc", "厕所", "卫生间" ->
443
+ RoutePOISearch.RoutePOISearchType.TypeToilet
444
+ "gasairstation", "gas_air_station", "gas-air-station", "fillingstation", "filling_station", "filling-station", "加气站", "加气" ->
445
+ RoutePOISearch.RoutePOISearchType.TypeFillingStation
446
+ "servicearea", "service_area", "service-area", "服务区" ->
447
+ RoutePOISearch.RoutePOISearchType.TypeServiceArea
448
+ "chargingpile", "charging_pile", "charging-pile", "chargestation", "charge_station", "charge-station", "充电桩", "充电站" ->
449
+ RoutePOISearch.RoutePOISearchType.TypeChargeStation
450
+ "food", "美食", "餐厅" ->
451
+ RoutePOISearch.RoutePOISearchType.TypeFood
452
+ "hotel", "酒店", "宾馆" ->
453
+ RoutePOISearch.RoutePOISearchType.TypeHotel
454
+ else -> throw IllegalArgumentException(
455
+ "Invalid routePoiType '$value'. Supported values: gasStation, maintenanceStation, atm, toilet, gasAirStation, serviceArea, chargingPile, food, hotel"
456
+ )
457
+ }
458
+ }
459
+
460
+ /**
461
+ * 确保 API Key 已设置
462
+ * 优先级:已设置 > AndroidManifest.xml
463
+ */
464
+ private fun ensureAPIKeyIsSet() {
465
+ val apiKey = try {
466
+ context.packageManager
467
+ .getApplicationInfo(context.packageName, android.content.pm.PackageManager.GET_META_DATA)
468
+ .metaData
469
+ ?.getString("com.amap.api.v2.apikey")
470
+ } catch (e: Exception) {
471
+ throw IllegalStateException("Failed to read AMap API Key from AndroidManifest.xml", e)
472
+ }
473
+
474
+ if (apiKey.isNullOrBlank()) {
475
+ throw IllegalStateException(
476
+ "AMap API Key is not configured. Set androidKey in the config plugin or add com.amap.api.v2.apikey to AndroidManifest.xml."
477
+ )
478
+ }
479
+
480
+ ServiceSettings.getInstance().setApiKey(apiKey)
481
+ }
482
+
483
+ /**
484
+ * 转换单个 POI Item
485
+ */
486
+ private fun convertPoiItem(poi: PoiItemV2, center: LatLonPoint? = null): Map<String, Any?> {
487
+ var distance = -1
488
+ if (center != null && poi.latLonPoint != null) {
489
+ val results = FloatArray(1)
490
+ android.location.Location.distanceBetween(
491
+ center.latitude, center.longitude,
492
+ poi.latLonPoint.latitude, poi.latLonPoint.longitude,
493
+ results
494
+ )
495
+ distance = results[0].toInt()
496
+ }
497
+
498
+ // 获取深度信息
499
+ val business = poi.business
500
+ val indoor = poi.indoorData
501
+ val photos = poi.photos
502
+
503
+ return mapOf<String, Any?>(
504
+ "id" to poi.poiId,
505
+ "name" to poi.title,
506
+ "address" to poi.snippet,
507
+ "location" to mapOf<String, Any?>(
508
+ "latitude" to poi.latLonPoint?.latitude,
509
+ "longitude" to poi.latLonPoint?.longitude
510
+ ),
511
+ "typeCode" to poi.typeCode,
512
+ "typeDes" to poi.typeDes,
513
+ "tel" to "", // V2 SDK 移除了 tel 字段,即使 ShowFields.ALL 也不包含
514
+ "distance" to distance,
515
+ "cityName" to poi.cityName,
516
+ "cityCode" to poi.cityCode,
517
+ "provinceName" to poi.provinceName,
518
+ "adName" to poi.adName,
519
+ "adCode" to poi.adCode,
520
+ // 添加深度信息字段映射
521
+ "business" to if (business != null) mapOf<String, Any?>(
522
+ "opentime" to business.opentimeWeek,
523
+ "opentimeToday" to business.opentimeToday,
524
+ "rating" to business.getmRating(),
525
+ "cost" to business.cost,
526
+ "parkingType" to business.parkingType,
527
+ "tag" to business.tag,
528
+ "tel" to business.tel,
529
+ "alias" to business.alias,
530
+ "businessArea" to business.businessArea
531
+ ) else null,
532
+ "photos" to photos?.map { photo ->
533
+ mapOf<String, Any?>(
534
+ "title" to photo.title,
535
+ "url" to photo.url
536
+ )
537
+ },
538
+ "indoor" to if (indoor != null) mapOf<String, Any?>(
539
+ "floor" to indoor.floor,
540
+ "floorName" to indoor.floorName,
541
+ "poiId" to indoor.poiId,
542
+ "hasIndoorMap" to indoor.isIndoorMap
543
+ ) else null
544
+ )
545
+ }
546
+
547
+ /**
548
+ * 转换单个 Legacy POI Item (用于逆地理编码)
549
+ */
550
+ private fun convertLegacyPoiItem(poi: PoiItem): Map<String, Any?> {
551
+ return mapOf<String, Any?>(
552
+ "id" to poi.poiId,
553
+ "name" to poi.title,
554
+ "address" to poi.snippet,
555
+ "location" to mapOf<String, Any?>(
556
+ "latitude" to poi.latLonPoint?.latitude,
557
+ "longitude" to poi.latLonPoint?.longitude
558
+ ),
559
+ "typeCode" to poi.typeCode,
560
+ "typeDes" to poi.typeDes,
561
+ "tel" to poi.tel,
562
+ "distance" to poi.distance,
563
+ "cityName" to poi.cityName,
564
+ "cityCode" to poi.cityCode,
565
+ "provinceName" to poi.provinceName,
566
+ "adName" to poi.adName,
567
+ "adCode" to poi.adCode,
568
+ "website" to poi.website,
569
+ "email" to poi.email,
570
+ "postcode" to poi.postcode,
571
+ "direction" to poi.direction,
572
+ "hasIndoorMap" to poi.isIndoorMap,
573
+ "businessArea" to poi.businessArea,
574
+ "parkingType" to poi.parkingType
575
+ )
576
+ }
577
+
578
+ /**
579
+ * 转换 POI 搜索结果
580
+ */
581
+ private fun convertPoiResult(result: PoiResultV2?, center: LatLonPoint? = null): Map<String, Any?> {
582
+ if (result == null) {
583
+ return mapOf(
584
+ "pois" to emptyList<Map<String, Any?>>(),
585
+ "total" to 0,
586
+ "pageNum" to 1,
587
+ "pageSize" to 20,
588
+ "pageCount" to 0
589
+ )
590
+ }
591
+
592
+ val pois = result.pois?.map { convertPoiItem(it, center) } ?: emptyList()
593
+
594
+ val totalCount = result.count
595
+ val pageSize = result.query.pageSize
596
+ val pageCount = if (pageSize > 0) (totalCount + pageSize - 1) / pageSize else 0
597
+
598
+ return mapOf(
599
+ "pois" to pois,
600
+ "total" to totalCount,
601
+ "pageNum" to (result.query.pageNum + 1),
602
+ "pageSize" to pageSize,
603
+ "pageCount" to pageCount
604
+ )
605
+ }
606
+
607
+ /**
608
+ * 转换输入提示结果
609
+ */
610
+ private fun convertTipsResult(tips: List<Tip>?): Map<String, Any?> {
611
+ val tipList = tips?.map { tip ->
612
+ mapOf(
613
+ "id" to tip.poiID,
614
+ "name" to tip.name,
615
+ "address" to tip.address,
616
+ "location" to tip.point?.let {
617
+ mapOf<String, Any?>(
618
+ "latitude" to it.latitude,
619
+ "longitude" to it.longitude
620
+ )
621
+ },
622
+ "typeCode" to tip.typeCode,
623
+ "cityName" to tip.district,
624
+ "adName" to tip.district
625
+ )
626
+ } ?: emptyList()
627
+
628
+ return mapOf("tips" to tipList)
629
+ }
630
+
631
+ /**
632
+ * 转换沿途 POI 搜索结果
633
+ */
634
+ private fun convertRoutePOIResult(result: RoutePOISearchResult): Map<String, Any?> {
635
+ val pois = result.routePois?.map { poi ->
636
+ mapOf<String, Any?>(
637
+ "id" to poi.id,
638
+ "name" to poi.title,
639
+ "address" to "", // RoutePOIItem 确实没有 address/snippet 字段
640
+ "location" to mapOf<String, Any?>(
641
+ "latitude" to poi.point?.latitude,
642
+ "longitude" to poi.point?.longitude
643
+ ),
644
+ "distance" to poi.distance
645
+ )
646
+ } ?: emptyList()
647
+
648
+ return mapOf(
649
+ "pois" to pois,
650
+ "total" to pois.size,
651
+ "pageNum" to 1,
652
+ "pageSize" to pois.size,
653
+ "pageCount" to 1
654
+ )
655
+ }
656
+
657
+ /**
658
+ * 转换逆地理编码结果
659
+ */
660
+ private fun convertRegeocodeResult(result: RegeocodeResult): Map<String, Any?> {
661
+ val address = result.regeocodeAddress ?: return emptyMap<String, Any?>()
662
+
663
+ val addressComponent = mapOf<String, Any?>(
664
+ "province" to address.province,
665
+ "city" to address.city,
666
+ "district" to address.district,
667
+ "township" to address.township,
668
+ "towncode" to address.towncode,
669
+ "neighborhood" to address.neighborhood,
670
+ "building" to address.building,
671
+ "cityCode" to address.cityCode,
672
+ "adCode" to address.adCode,
673
+ "country" to address.country,
674
+ "countryCode" to address.countryCode,
675
+ "streetNumber" to (address.streetNumber?.let {
676
+ mapOf<String, Any?>(
677
+ "street" to it.street,
678
+ "number" to it.number,
679
+ "direction" to it.direction,
680
+ "distance" to it.distance
681
+ )
682
+ } ?: mapOf<String, Any?>(
683
+ "street" to "",
684
+ "number" to "",
685
+ "direction" to "",
686
+ "distance" to 0f
687
+ )),
688
+ "businessAreas" to (address.businessAreas?.map { area ->
689
+ mapOf<String, Any?>(
690
+ "name" to area.name,
691
+ "location" to mapOf<String, Any?>(
692
+ "latitude" to area.centerPoint?.latitude,
693
+ "longitude" to area.centerPoint?.longitude
694
+ )
695
+ )
696
+ } ?: emptyList())
697
+ )
698
+
699
+ val pois = address.pois?.map { convertLegacyPoiItem(it) } ?: emptyList()
700
+
701
+ val aois = address.aois?.map { aoi ->
702
+ mapOf<String, Any?>(
703
+ "id" to aoi.aoiId,
704
+ "name" to aoi.aoiName,
705
+ "adCode" to aoi.adCode,
706
+ "location" to mapOf<String, Any?>(
707
+ "latitude" to aoi.aoiCenterPoint?.latitude,
708
+ "longitude" to aoi.aoiCenterPoint?.longitude
709
+ ),
710
+ "area" to aoi.aoiArea
711
+ )
712
+ } ?: emptyList()
713
+
714
+ val roads = address.roads?.map { road ->
715
+ mapOf<String, Any?>(
716
+ "id" to road.id,
717
+ "name" to road.name,
718
+ "distance" to road.distance,
719
+ "direction" to road.direction,
720
+ "location" to mapOf<String, Any?>(
721
+ "latitude" to road.latLngPoint?.latitude,
722
+ "longitude" to road.latLngPoint?.longitude
723
+ )
724
+ )
725
+ } ?: emptyList()
726
+
727
+ val roadCrosses = address.crossroads?.map { cross ->
728
+ mapOf<String, Any?>(
729
+ "distance" to cross.distance,
730
+ "direction" to cross.direction,
731
+ "location" to mapOf<String, Any?>(
732
+ "latitude" to cross.centerPoint?.latitude,
733
+ "longitude" to cross.centerPoint?.longitude
734
+ ),
735
+ "firstId" to cross.firstRoadId,
736
+ "firstName" to cross.firstRoadName,
737
+ "secondId" to cross.secondRoadId,
738
+ "secondName" to cross.secondRoadName
739
+ )
740
+ } ?: emptyList()
741
+
742
+ return mapOf(
743
+ "formattedAddress" to address.formatAddress,
744
+ "addressComponent" to addressComponent,
745
+ "pois" to pois,
746
+ "aois" to aois,
747
+ "roads" to roads,
748
+ "roadCrosses" to roadCrosses
749
+ )
750
+ }
751
+ }