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.
- package/README.md +20 -14
- package/android/build.gradle +8 -4
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapModule.kt +7 -8
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapOfflineModule.kt +150 -27
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +24 -14
- package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +38 -41
- package/android/src/main/java/expo/modules/gaodemap/modules/SDKInitializer.kt +18 -17
- package/android/src/main/java/expo/modules/gaodemap/overlays/CircleView.kt +3 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +6 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +124 -10
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +2 -2
- package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerBitmapRenderer.kt +10 -9
- package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +7 -11
- package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +3 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/PolygonView.kt +2 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/PolylineView.kt +1 -0
- package/android/src/main/java/expo/modules/gaodemap/search/ExpoGaodeMapSearchModule.kt +751 -0
- package/android/src/main/java/expo/modules/gaodemap/utils/GeometryUtils.kt +5 -5
- package/android/src/main/java/expo/modules/gaodemap/utils/PermissionHelper.kt +13 -16
- package/build/ExpoGaodeMapOfflineModule.d.ts +5 -0
- package/build/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
- package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
- package/build/components/overlays/HeatMap.d.ts.map +1 -1
- package/build/components/overlays/HeatMap.js +21 -2
- package/build/components/overlays/HeatMap.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/search/ExpoGaodeMapSearch.types.d.ts +340 -0
- package/build/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
- package/build/search/ExpoGaodeMapSearch.types.js +19 -0
- package/build/search/ExpoGaodeMapSearch.types.js.map +1 -0
- package/build/search/ExpoGaodeMapSearchModule.d.ts +74 -0
- package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
- package/build/search/ExpoGaodeMapSearchModule.js +47 -0
- package/build/search/ExpoGaodeMapSearchModule.js.map +1 -0
- package/build/search/index.d.ts +156 -0
- package/build/search/index.d.ts.map +1 -0
- package/build/search/index.js +171 -0
- package/build/search/index.js.map +1 -0
- package/build/types/map-view.types.d.ts +4 -2
- package/build/types/map-view.types.d.ts.map +1 -1
- package/build/types/map-view.types.js.map +1 -1
- package/build/utils/OfflineMapManager.d.ts +4 -0
- package/build/utils/OfflineMapManager.d.ts.map +1 -1
- package/build/utils/OfflineMapManager.js +6 -0
- package/build/utils/OfflineMapManager.js.map +1 -1
- package/expo-module.config.json +4 -2
- package/ios/ExpoGaodeMap.podspec +2 -2
- package/ios/ExpoGaodeMapOfflineModule.swift +60 -0
- package/ios/ExpoGaodeMapSearchModule.swift +773 -0
- package/ios/modules/LocationManager.swift +9 -3
- package/ios/overlays/PolylineView.swift +6 -12
- package/package.json +1 -1
- 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
|
+
}
|