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,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
|
+
}
|