expo-gaode-map-navigation 2.0.6 → 2.0.7

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 (30) hide show
  1. package/README.md +25 -0
  2. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +71 -45
  3. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +35 -0
  4. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +0 -1
  5. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +13 -2
  6. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNavigationModule.kt +385 -1
  7. package/android/src/main/java/expo/modules/gaodemap/navigation/routes/drive/DriveTruckRouteCalculator.kt +180 -4
  8. package/android/src/main/java/expo/modules/gaodemap/navigation/services/IndependentRouteService.kt +182 -7
  9. package/android/src/main/java/expo/modules/gaodemap/navigation/utils/Converters.kt +31 -1
  10. package/build/index.d.ts +7 -2
  11. package/build/index.d.ts.map +1 -1
  12. package/build/index.js +5 -0
  13. package/build/index.js.map +1 -1
  14. package/build/types/independent.types.d.ts +7 -0
  15. package/build/types/independent.types.d.ts.map +1 -1
  16. package/build/types/independent.types.js.map +1 -1
  17. package/build/types/native-module.types.d.ts +5 -1
  18. package/build/types/native-module.types.d.ts.map +1 -1
  19. package/build/types/native-module.types.js.map +1 -1
  20. package/build/types/route.types.d.ts +112 -0
  21. package/build/types/route.types.d.ts.map +1 -1
  22. package/build/types/route.types.js.map +1 -1
  23. package/ios/ExpoGaodeMapNaviView.swift +56 -4
  24. package/ios/ExpoGaodeMapNavigationModule.swift +585 -12
  25. package/ios/map/ExpoGaodeMapOfflineModule.swift +58 -34
  26. package/ios/map/GaodeMapPrivacyManager.swift +23 -1
  27. package/ios/map/overlays/MarkerView.swift +148 -11
  28. package/ios/services/IndependentRouteService.swift +186 -44
  29. package/package.json +1 -1
  30. package/plugin/build/withGaodeMap.js +28 -0
@@ -12,6 +12,7 @@
12
12
 
13
13
  import ExpoModulesCore
14
14
  import AMapNaviKit
15
+ import UIKit
15
16
 
16
17
  public class ExpoGaodeMapNavigationModule: Module {
17
18
 
@@ -23,6 +24,9 @@ public class ExpoGaodeMapNavigationModule: Module {
23
24
  private let independentRouteManager = IndependentRouteManager()
24
25
  private var independentRouteService: IndependentRouteService?
25
26
 
27
+ // 官方导航组件(iOS)
28
+ private var officialCompositeManager: AMapNaviCompositeManager?
29
+
26
30
  // MARK: - 高德 SDK 初始化检查
27
31
 
28
32
  /// 检查高德地图 SDK 是否已初始化
@@ -66,11 +70,55 @@ public class ExpoGaodeMapNavigationModule: Module {
66
70
 
67
71
  /// 在需要使用导航功能前进行初始化检查
68
72
  private func ensureInitialized() throws {
73
+ try ensurePrivacyReady()
69
74
  try checkAMapInitialization()
70
75
  }
76
+
77
+ /// 检查 iOS 是否开启后台定位模式(避免导航启动时出现系统异常)
78
+ private func ensureBackgroundLocationModeForNavigation() throws {
79
+ let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
80
+ guard backgroundModes?.contains("location") == true else {
81
+ throw NSError(
82
+ domain: "ExpoGaodeMapNavigation",
83
+ code: -1003,
84
+ userInfo: [
85
+ NSLocalizedDescriptionKey: "iOS 后台定位模式未开启,无法安全启动导航",
86
+ NSLocalizedFailureReasonErrorKey: "Info.plist 缺少 UIBackgroundModes: location",
87
+ NSLocalizedRecoverySuggestionErrorKey: """
88
+ 请在 app.json 插件配置中开启后台定位后重新 prebuild:
89
+ ["expo-gaode-map-navigation", {
90
+ "enableBackgroundLocation": true
91
+ }]
92
+ 然后执行:npx expo prebuild --clean
93
+ """
94
+ ]
95
+ )
96
+ }
97
+ }
98
+
99
+ /// 检查并同步隐私状态(导航 SDK 同样要求在调用任何接口前完成)
100
+ private func ensurePrivacyReady() throws {
101
+ GaodeMapPrivacyManager.restorePersistedState()
102
+ guard GaodeMapPrivacyManager.isReady else {
103
+ throw NSError(
104
+ domain: "ExpoGaodeMapNavigation",
105
+ code: -1002,
106
+ userInfo: [
107
+ NSLocalizedDescriptionKey: "隐私协议未完成确认",
108
+ NSLocalizedFailureReasonErrorKey: "请在调用导航能力前先完成隐私协议弹窗与同意",
109
+ NSLocalizedRecoverySuggestionErrorKey: "请先调用 setPrivacyConfig({ hasShow: true, hasContainsPrivacy: true, hasAgree: true })"
110
+ ]
111
+ )
112
+ }
113
+ GaodeMapPrivacyManager.applyPrivacyState()
114
+ }
71
115
 
72
116
  public func definition() -> ModuleDefinition {
73
117
  Name("ExpoGaodeMapNavigation")
118
+
119
+ OnCreate {
120
+ GaodeMapPrivacyManager.restorePersistedState()
121
+ }
74
122
 
75
123
  Events(
76
124
  "onCalculateRouteSuccess",
@@ -83,6 +131,7 @@ public class ExpoGaodeMapNavigationModule: Module {
83
131
  driveTruckCalculator = nil
84
132
  walkRideCalculator = nil
85
133
  independentRouteService = nil
134
+ officialCompositeManager = nil
86
135
  independentRouteManager.clearAll()
87
136
  }
88
137
 
@@ -94,7 +143,6 @@ public class ExpoGaodeMapNavigationModule: Module {
94
143
  self.ensureWalkRide()
95
144
  return true
96
145
  } catch {
97
- let errorMessage = self.formatError(error)
98
146
  throw error
99
147
  }
100
148
  }
@@ -106,6 +154,7 @@ public class ExpoGaodeMapNavigationModule: Module {
106
154
  self.driveTruckCalculator = nil
107
155
  self.walkRideCalculator = nil
108
156
  self.independentRouteService = nil
157
+ self.officialCompositeManager = nil
109
158
  self.independentRouteManager.clearAll()
110
159
  }
111
160
 
@@ -116,7 +165,7 @@ public class ExpoGaodeMapNavigationModule: Module {
116
165
  try self.ensureInitialized()
117
166
  self.ensureDriveTruck().calculateDriveRoute(options: options, promise: promise)
118
167
  } catch {
119
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
168
+ self.rejectInitializationError(promise, error: error)
120
169
  }
121
170
  }
122
171
 
@@ -125,7 +174,7 @@ public class ExpoGaodeMapNavigationModule: Module {
125
174
  try self.ensureInitialized()
126
175
  self.ensureDriveTruck().calculateTruckRoute(options: options, promise: promise)
127
176
  } catch {
128
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
177
+ self.rejectInitializationError(promise, error: error)
129
178
  }
130
179
  }
131
180
 
@@ -136,7 +185,7 @@ public class ExpoGaodeMapNavigationModule: Module {
136
185
  try self.ensureInitialized()
137
186
  self.ensureWalkRide().calculateWalkRoute(options: options, promise: promise)
138
187
  } catch {
139
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
188
+ self.rejectInitializationError(promise, error: error)
140
189
  }
141
190
  }
142
191
 
@@ -145,7 +194,7 @@ public class ExpoGaodeMapNavigationModule: Module {
145
194
  try self.ensureInitialized()
146
195
  self.ensureWalkRide().calculateRideRoute(options: options, promise: promise)
147
196
  } catch {
148
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
197
+ self.rejectInitializationError(promise, error: error)
149
198
  }
150
199
  }
151
200
 
@@ -156,7 +205,7 @@ public class ExpoGaodeMapNavigationModule: Module {
156
205
  try self.ensureInitialized()
157
206
  self.ensureIndependentService().independentDriveRoute(options: options, promise: promise)
158
207
  } catch {
159
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
208
+ self.rejectInitializationError(promise, error: error)
160
209
  }
161
210
  }
162
211
 
@@ -165,7 +214,7 @@ public class ExpoGaodeMapNavigationModule: Module {
165
214
  try self.ensureInitialized()
166
215
  self.ensureIndependentService().independentTruckRoute(options: options, promise: promise)
167
216
  } catch {
168
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
217
+ self.rejectInitializationError(promise, error: error)
169
218
  }
170
219
  }
171
220
 
@@ -174,7 +223,7 @@ public class ExpoGaodeMapNavigationModule: Module {
174
223
  try self.ensureInitialized()
175
224
  self.ensureIndependentService().independentWalkRoute(options: options, promise: promise)
176
225
  } catch {
177
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
226
+ self.rejectInitializationError(promise, error: error)
178
227
  }
179
228
  }
180
229
 
@@ -183,7 +232,7 @@ public class ExpoGaodeMapNavigationModule: Module {
183
232
  try self.ensureInitialized()
184
233
  self.ensureIndependentService().independentRideRoute(options: options, promise: promise)
185
234
  } catch {
186
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
235
+ self.rejectInitializationError(promise, error: error)
187
236
  }
188
237
  }
189
238
 
@@ -192,7 +241,7 @@ public class ExpoGaodeMapNavigationModule: Module {
192
241
  try self.ensureInitialized()
193
242
  self.ensureIndependentService().independentMotorcycleRoute(options: options, promise: promise)
194
243
  } catch {
195
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
244
+ self.rejectInitializationError(promise, error: error)
196
245
  }
197
246
  }
198
247
 
@@ -210,6 +259,13 @@ public class ExpoGaodeMapNavigationModule: Module {
210
259
 
211
260
  // 使用独立路径启动导航
212
261
  AsyncFunction("startNaviWithIndependentPath") { (options: [String: Any], promise: Promise) in
262
+ do {
263
+ try self.ensureInitialized()
264
+ try self.ensureBackgroundLocationModeForNavigation()
265
+ } catch {
266
+ self.rejectInitializationError(promise, error: error)
267
+ return
268
+ }
213
269
  guard let token = options["token"] as? Int else {
214
270
  promise.reject("INVALID_TOKEN", "token is required")
215
271
  return
@@ -220,6 +276,23 @@ public class ExpoGaodeMapNavigationModule: Module {
220
276
  let ok = self.independentRouteManager.start(token: token, naviType: naviType, routeId: routeId, routeIndex: routeIndex)
221
277
  promise.resolve(ok)
222
278
  }
279
+
280
+ // 官方导航页(iOS: AMapNaviCompositeManager)
281
+ AsyncFunction("openOfficialNaviPage") { (options: [String: Any], promise: Promise) in
282
+ do {
283
+ try self.ensureInitialized()
284
+ let pageType = (options["pageType"] as? String)?.uppercased()
285
+ let startNaviDirectly = (options["startNaviDirectly"] as? Bool) ?? false
286
+ // 只有直接进导航页时才强制要求后台定位模式;纯路线规划页不拦截
287
+ if pageType == "NAVI" || startNaviDirectly {
288
+ try self.ensureBackgroundLocationModeForNavigation()
289
+ }
290
+ } catch {
291
+ self.rejectInitializationError(promise, error: error)
292
+ return
293
+ }
294
+ self.openOfficialNaviPage(options: options, promise: promise)
295
+ }
223
296
 
224
297
  // 清除独立路线
225
298
  AsyncFunction("clearIndependentRoute") { (options: [String: Any], promise: Promise) in
@@ -239,7 +312,7 @@ public class ExpoGaodeMapNavigationModule: Module {
239
312
  // 摩托车复用驾车算路
240
313
  self.ensureDriveTruck().calculateDriveRoute(options: options, promise: promise)
241
314
  } catch {
242
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
315
+ self.rejectInitializationError(promise, error: error)
243
316
  }
244
317
  }
245
318
 
@@ -251,7 +324,7 @@ public class ExpoGaodeMapNavigationModule: Module {
251
324
  // 电动车复用骑行算路
252
325
  self.ensureWalkRide().calculateRideRoute(options: options, promise: promise)
253
326
  } catch {
254
- promise.reject("AMAP_NOT_INITIALIZED", self.formatError(error))
327
+ self.rejectInitializationError(promise, error: error)
255
328
  }
256
329
  }
257
330
  }
@@ -296,4 +369,504 @@ public class ExpoGaodeMapNavigationModule: Module {
296
369
 
297
370
  return message
298
371
  }
372
+
373
+ private func rejectInitializationError(_ promise: Promise, error: Error) {
374
+ let nsError = error as NSError
375
+ let code: String
376
+ if nsError.domain == "ExpoGaodeMapNavigation" && nsError.code == -1002 {
377
+ code = "PRIVACY_NOT_AGREED"
378
+ } else if nsError.domain == "ExpoGaodeMapNavigation" && nsError.code == -1003 {
379
+ code = "BACKGROUND_LOCATION_NOT_ENABLED"
380
+ } else {
381
+ code = "AMAP_NOT_INITIALIZED"
382
+ }
383
+ promise.reject(code, formatError(error))
384
+ }
385
+
386
+ // MARK: - iOS 官方导航组件
387
+
388
+ private func openOfficialNaviPage(options: [String: Any], promise: Promise) {
389
+ DispatchQueue.main.async {
390
+ do {
391
+ let config = try self.createOfficialCompositeConfig(options: options)
392
+ let manager = self.officialCompositeManager ?? AMapNaviCompositeManager()
393
+ self.officialCompositeManager = manager
394
+ self.invokeObjectSetter(
395
+ target: manager,
396
+ selectorName: "presentRoutePlanViewControllerWithOptions:",
397
+ value: config
398
+ )
399
+ promise.resolve(true)
400
+ } catch {
401
+ promise.reject("OPEN_OFFICIAL_NAVI_PAGE_FAILED", self.formatError(error))
402
+ }
403
+ }
404
+ }
405
+
406
+ private func createOfficialCompositeConfig(options: [String: Any]) throws -> AMapNaviCompositeUserConfig {
407
+ let config = AMapNaviCompositeUserConfig()
408
+
409
+ if let presenter = currentPresenterViewController() {
410
+ invokeObjectSetter(target: config, selectorName: "setPresenterViewController:", value: presenter)
411
+ }
412
+
413
+ if let from = options["from"] as? [String: Any] {
414
+ try setRoutePlanPOI(
415
+ config: config,
416
+ poiType: 0,
417
+ coordinate: from,
418
+ fallbackName: "起点"
419
+ )
420
+ }
421
+
422
+ guard let to = options["to"] as? [String: Any] else {
423
+ throw NSError(
424
+ domain: "ExpoGaodeMapNavigation",
425
+ code: -2001,
426
+ userInfo: [NSLocalizedDescriptionKey: "openOfficialNaviPage 参数缺少 to(终点)"]
427
+ )
428
+ }
429
+ try setRoutePlanPOI(
430
+ config: config,
431
+ poiType: 1,
432
+ coordinate: to,
433
+ fallbackName: "终点"
434
+ )
435
+
436
+ if let waypoints = options["waypoints"] as? [Any] {
437
+ for (index, item) in waypoints.prefix(3).enumerated() {
438
+ guard let dict = item as? [String: Any] else { continue }
439
+ try setRoutePlanPOI(
440
+ config: config,
441
+ poiType: 2,
442
+ coordinate: dict,
443
+ fallbackName: "途经点\(index + 1)"
444
+ )
445
+ }
446
+ }
447
+
448
+ // let startNaviDirectly = boolValue(options["startNaviDirectly"])
449
+ // ?? ((options["pageType"] as? String)?.uppercased() == "NAVI")
450
+ // if let startNaviDirectly {
451
+ // invokeBoolSetter(target: config, selectorName: "setStartNaviDirectly:", value: startNaviDirectly)
452
+ // }
453
+ let startNaviDirectly = boolValue(options["startNaviDirectly"])
454
+ ?? ((options["pageType"] as? String)?.uppercased() == "NAVI")
455
+
456
+ invokeBoolSetter(
457
+ target: config,
458
+ selectorName: "setStartNaviDirectly:",
459
+ value: startNaviDirectly
460
+ )
461
+
462
+ // iOS 官方导航组件模式目前仅支持实时导航,不支持模拟导航。
463
+ if startNaviDirectly && isCompositeEmulatorRequested(options: options) {
464
+ throw NSError(
465
+ domain: "ExpoGaodeMapNavigation",
466
+ code: -2006,
467
+ userInfo: [NSLocalizedDescriptionKey: "iOS 官方导航组件模式不支持模拟导航,请改用实时导航(naviMode=1)或使用 NaviView 模拟导航"]
468
+ )
469
+ }
470
+
471
+ if let needCalculateRoute = boolValue(options["needCalculateRouteWhenPresent"]) {
472
+ invokeBoolSetter(target: config, selectorName: "setNeedCalculateRouteWhenPresent:", value: needCalculateRoute)
473
+ }
474
+ if let needDestroy = boolValue(options["needDestroyDriveManagerInstanceWhenNaviExit"]) {
475
+ invokeBoolSetter(target: config, selectorName: "setNeedDestoryDriveManagerInstanceWhenDismiss:", value: needDestroy)
476
+ }
477
+ if let showExit = boolValue(options["showExitNaviDialog"]) {
478
+ invokeBoolSetter(target: config, selectorName: "setNeedShowConfirmViewWhenStopGPSNavi:", value: showExit)
479
+ }
480
+ if let showNextRoadInfo = boolValue(options["showNextRoadInfo"]) {
481
+ invokeBoolSetter(target: config, selectorName: "setShowNextRoadInfo:", value: showNextRoadInfo)
482
+ }
483
+ if let showCrossImage = boolValue(options["showCrossImage"]) {
484
+ invokeBoolSetter(target: config, selectorName: "setShowCrossImage:", value: showCrossImage)
485
+ }
486
+ if let showPreference = boolValue(options["showRouteStrategyPreferenceView"]) {
487
+ invokeBoolSetter(target: config, selectorName: "setShowDrivingStrategyPreferenceView:", value: showPreference)
488
+ }
489
+ if let multipleRoute = boolValue(options["multipleRouteNaviMode"]) {
490
+ invokeBoolSetter(target: config, selectorName: "setMultipleRouteNaviMode:", value: multipleRoute)
491
+ }
492
+ if let truckMultipleRoute = boolValue(options["truckMultipleRouteNaviMode"]) {
493
+ invokeBoolSetter(target: config, selectorName: "setTruckMultipleRouteNaviMode:", value: truckMultipleRoute)
494
+ }
495
+ if let showBackupRoute = boolValue(options["showBackupRoute"]) {
496
+ invokeBoolSetter(target: config, selectorName: "setShowBackupRoute:", value: showBackupRoute)
497
+ }
498
+ if let trafficEnabled = boolValue(options["trafficEnabled"]) {
499
+ invokeBoolSetter(target: config, selectorName: "setMapShowTraffic:", value: trafficEnabled)
500
+ }
501
+ if let autoZoom = boolValue(options["scaleAutoChangeEnable"]) {
502
+ invokeBoolSetter(target: config, selectorName: "setAutoZoomMapLevel:", value: autoZoom)
503
+ }
504
+ if let showEagleMap = boolValue(options["showEagleMap"]) {
505
+ invokeBoolSetter(target: config, selectorName: "setShowEagleMap:", value: showEagleMap)
506
+ }
507
+ if let showCameraDistanceEnable = boolValue(options["showCameraDistanceEnable"]) {
508
+ invokeBoolSetter(
509
+ target: config,
510
+ selectorName: "setShowCameraDistanceEnable:",
511
+ value: showCameraDistanceEnable
512
+ )
513
+ }
514
+ if let scaleFactor = doubleValue(options["scaleFactor"]) {
515
+ invokeDoubleSetter(target: config, selectorName: "setScaleFactor:", value: scaleFactor)
516
+ }
517
+ if let showRestrict = boolValue(options["showRestrictareaEnable"]) {
518
+ invokeBoolSetter(target: config, selectorName: "setShowRestrictareaEnable:", value: showRestrict)
519
+ }
520
+ if let removePolyline = boolValue(options["removePolylineAndVectorlineWhenArrivedDestination"]) {
521
+ invokeBoolSetter(
522
+ target: config,
523
+ selectorName: "setRemovePolylineAndVectorlineWhenArrivedDestination:",
524
+ value: removePolyline
525
+ )
526
+ }
527
+
528
+ if let routeStrategy = intValue(options["routeStrategy"]) {
529
+ invokeIntSetter(target: config, selectorName: "setDriveStrategy:", value: routeStrategy)
530
+ }
531
+ if let onlineCarHailingType = intValue(options["onlineCarHailingType"]) {
532
+ _ = invokeIntReturnBoolSetter(
533
+ target: config,
534
+ selectorName: "setOnlineCarHailingType:",
535
+ value: onlineCarHailingType
536
+ )
537
+ }
538
+
539
+ if let themeRaw = parseThemeType(options["theme"]) {
540
+ invokeIntSetter(target: config, selectorName: "setThemeType:", value: themeRaw)
541
+ }
542
+ if let mapMode = parseMapViewModeType(options: options) {
543
+ invokeIntSetter(target: config, selectorName: "setMapViewModeType:", value: mapMode)
544
+ }
545
+ if let broadcastType = parseBroadcastType(options: options) {
546
+ invokeUIntSetter(target: config, selectorName: "setBroadcastType:", value: UInt(broadcastType))
547
+ }
548
+ if let trackingMode = parseTrackingMode(options: options) {
549
+ invokeIntSetter(target: config, selectorName: "setTrackingMode:", value: trackingMode)
550
+ }
551
+
552
+ if let carInfo = options["carInfo"] as? [String: Any],
553
+ let vehicleInfo = buildVehicleInfo(options: carInfo) {
554
+ invokeObjectSetter(target: config, selectorName: "setVehicleInfo:", value: vehicleInfo)
555
+ }
556
+
557
+ return config
558
+ }
559
+
560
+ private func setRoutePlanPOI(
561
+ config: AMapNaviCompositeUserConfig,
562
+ poiType: Int,
563
+ coordinate: [String: Any],
564
+ fallbackName: String
565
+ ) throws {
566
+ guard let latitude = doubleValue(coordinate["latitude"]),
567
+ let longitude = doubleValue(coordinate["longitude"]) else {
568
+ throw NSError(
569
+ domain: "ExpoGaodeMapNavigation",
570
+ code: -2002,
571
+ userInfo: [NSLocalizedDescriptionKey: "坐标参数必须包含有效的 latitude/longitude"]
572
+ )
573
+ }
574
+
575
+ let createdPoint = AMapNaviPoint.location(withLatitude: CGFloat(latitude), longitude: CGFloat(longitude))
576
+ guard let point = (createdPoint as AMapNaviPoint?) else {
577
+ throw NSError(
578
+ domain: "ExpoGaodeMapNavigation",
579
+ code: -2005,
580
+ userInfo: [NSLocalizedDescriptionKey: "创建导航坐标失败"]
581
+ )
582
+ }
583
+ let name = ((coordinate["name"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
584
+ ? (coordinate["name"] as? String)
585
+ : fallbackName
586
+ let poiId = (coordinate["poiId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
587
+
588
+ let selector = NSSelectorFromString("setRoutePlanPOIType:location:name:POIId:")
589
+ guard config.responds(to: selector) else {
590
+ throw NSError(
591
+ domain: "ExpoGaodeMapNavigation",
592
+ code: -2003,
593
+ userInfo: [NSLocalizedDescriptionKey: "当前 iOS SDK 不支持 setRoutePlanPOIType 接口"]
594
+ )
595
+ }
596
+
597
+ typealias Function = @convention(c) (
598
+ AnyObject,
599
+ Selector,
600
+ Int,
601
+ AMapNaviPoint,
602
+ NSString?,
603
+ NSString?
604
+ ) -> Bool
605
+ let implementation = config.method(for: selector)
606
+ let function = unsafeBitCast(implementation, to: Function.self)
607
+ let success = function(config, selector, poiType, point, name as NSString?, poiId as NSString?)
608
+ if !success {
609
+ throw NSError(
610
+ domain: "ExpoGaodeMapNavigation",
611
+ code: -2004,
612
+ userInfo: [NSLocalizedDescriptionKey: "设置导航 POI 失败,请检查坐标或 POI 参数"]
613
+ )
614
+ }
615
+ }
616
+
617
+ private func buildVehicleInfo(options: [String: Any]) -> AMapNaviVehicleInfo? {
618
+ let vehicleInfo = AMapNaviVehicleInfo()
619
+ var hasAnyValue = false
620
+
621
+ if let vehicleId = nonEmptyString(options["carNumber"]) {
622
+ vehicleInfo.vehicleId = vehicleId
623
+ hasAnyValue = true
624
+ }
625
+ if let restriction = boolValue(options["restriction"]) {
626
+ vehicleInfo.isETARestriction = restriction
627
+ hasAnyValue = true
628
+ }
629
+ if let type = intValue(options["carType"]) ?? intValue(options["type"]) {
630
+ vehicleInfo.type = type
631
+ hasAnyValue = true
632
+ }
633
+ if let motorcycleCC = intValue(options["motorcycleCC"]) {
634
+ vehicleInfo.motorcycleCC = motorcycleCC
635
+ hasAnyValue = true
636
+ }
637
+ if let size = intValue(options["vehicleSize"]) {
638
+ vehicleInfo.size = size
639
+ hasAnyValue = true
640
+ }
641
+ if let axisNums = intValue(options["vehicleAxis"]) {
642
+ vehicleInfo.axisNums = axisNums
643
+ hasAnyValue = true
644
+ }
645
+ if let width = doubleValue(options["vehicleWidth"]) {
646
+ vehicleInfo.width = CGFloat(width)
647
+ hasAnyValue = true
648
+ }
649
+ if let height = doubleValue(options["vehicleHeight"]) {
650
+ vehicleInfo.height = CGFloat(height)
651
+ hasAnyValue = true
652
+ }
653
+ if let length = doubleValue(options["vehicleLength"]) {
654
+ vehicleInfo.length = CGFloat(length)
655
+ hasAnyValue = true
656
+ }
657
+ if let load = doubleValue(options["vehicleLoad"]) {
658
+ vehicleInfo.load = CGFloat(load)
659
+ hasAnyValue = true
660
+ }
661
+ if let weight = doubleValue(options["vehicleWeight"]) {
662
+ vehicleInfo.weight = CGFloat(weight)
663
+ hasAnyValue = true
664
+ }
665
+ if let isLoadIgnore = boolValue(options["isLoadIgnore"]) {
666
+ vehicleInfo.isLoadIgnore = isLoadIgnore
667
+ hasAnyValue = true
668
+ }
669
+
670
+ return hasAnyValue ? vehicleInfo : nil
671
+ }
672
+
673
+ private func parseThemeType(_ raw: Any?) -> Int? {
674
+ if let value = intValue(raw) {
675
+ return value
676
+ }
677
+ guard let name = (raw as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).uppercased(),
678
+ !name.isEmpty else {
679
+ return nil
680
+ }
681
+ switch name {
682
+ case "BLUE", "DEFAULT":
683
+ return 0
684
+ case "WHITE", "LIGHT":
685
+ return 1
686
+ case "BLACK", "DARK":
687
+ return 2
688
+ default:
689
+ return nil
690
+ }
691
+ }
692
+
693
+ private func parseMapViewModeType(options: [String: Any]) -> Int? {
694
+ if let raw = intValue(options["mapViewModeType"]) {
695
+ return raw
696
+ }
697
+ guard let dayNightMode = intValue(options["dayAndNightMode"]) else {
698
+ return nil
699
+ }
700
+ switch dayNightMode {
701
+ case 0:
702
+ return 2 // Android: 自动 -> iOS: DayNightAuto
703
+ case 1:
704
+ return 0 // Android: 白天 -> iOS: Day
705
+ case 2:
706
+ return 1 // Android: 夜间 -> iOS: Night
707
+ default:
708
+ return dayNightMode
709
+ }
710
+ }
711
+
712
+ private func isCompositeEmulatorRequested(options: [String: Any]) -> Bool {
713
+ if let naviMode = intValue(options["naviMode"]), naviMode == 2 {
714
+ return true
715
+ }
716
+ // 历史参数兼容:iosNaviMode 已废弃,但若仍传 2,依然给出明确不支持提示
717
+ if let legacyIOSNaviMode = intValue(options["iosNaviMode"]), legacyIOSNaviMode == 2 {
718
+ return true
719
+ }
720
+ return false
721
+ }
722
+
723
+ private func parseBroadcastType(options: [String: Any]) -> Int? {
724
+ if let raw = intValue(options["broadcastType"]) {
725
+ return raw
726
+ }
727
+ guard let broadcastMode = intValue(options["broadcastMode"]) else {
728
+ return nil
729
+ }
730
+ switch broadcastMode {
731
+ case 1:
732
+ return 1 // Android: 简洁 -> iOS: concise
733
+ case 2:
734
+ return 0 // Android: 详细 -> iOS: detailed
735
+ case 3:
736
+ return 2 // Android: 静音 -> iOS: mute
737
+ default:
738
+ return broadcastMode
739
+ }
740
+ }
741
+
742
+ private func parseTrackingMode(options: [String: Any]) -> Int? {
743
+ if let raw = intValue(options["trackingMode"]) {
744
+ return raw
745
+ }
746
+ guard let carDirectionMode = intValue(options["carDirectionMode"]) else {
747
+ return nil
748
+ }
749
+ switch carDirectionMode {
750
+ case 1:
751
+ return 0 // Android: 正北向上 -> iOS: map north
752
+ case 2:
753
+ return 1 // Android: 车头向上 -> iOS: car north
754
+ default:
755
+ return carDirectionMode
756
+ }
757
+ }
758
+
759
+ private func currentPresenterViewController() -> UIViewController? {
760
+ guard let scene = UIApplication.shared.connectedScenes
761
+ .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene else {
762
+ return UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController
763
+ }
764
+ let root = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController
765
+ ?? scene.windows.first?.rootViewController
766
+ return topViewController(from: root)
767
+ }
768
+
769
+ private func topViewController(from root: UIViewController?) -> UIViewController? {
770
+ guard let root else { return nil }
771
+ if let presented = root.presentedViewController {
772
+ return topViewController(from: presented)
773
+ }
774
+ if let nav = root as? UINavigationController {
775
+ return topViewController(from: nav.visibleViewController)
776
+ }
777
+ if let tab = root as? UITabBarController {
778
+ return topViewController(from: tab.selectedViewController)
779
+ }
780
+ return root
781
+ }
782
+
783
+ private func invokeBoolSetter(target: NSObject, selectorName: String, value: Bool) {
784
+ let selector = NSSelectorFromString(selectorName)
785
+ guard target.responds(to: selector) else { return }
786
+ typealias Function = @convention(c) (AnyObject, Selector, Bool) -> Void
787
+ let implementation = target.method(for: selector)
788
+ let function = unsafeBitCast(implementation, to: Function.self)
789
+ function(target, selector, value)
790
+ }
791
+
792
+ private func invokeIntSetter(target: NSObject, selectorName: String, value: Int) {
793
+ let selector = NSSelectorFromString(selectorName)
794
+ guard target.responds(to: selector) else { return }
795
+ typealias Function = @convention(c) (AnyObject, Selector, Int) -> Void
796
+ let implementation = target.method(for: selector)
797
+ let function = unsafeBitCast(implementation, to: Function.self)
798
+ function(target, selector, value)
799
+ }
800
+
801
+ private func invokeUIntSetter(target: NSObject, selectorName: String, value: UInt) {
802
+ let selector = NSSelectorFromString(selectorName)
803
+ guard target.responds(to: selector) else { return }
804
+ typealias Function = @convention(c) (AnyObject, Selector, UInt) -> Void
805
+ let implementation = target.method(for: selector)
806
+ let function = unsafeBitCast(implementation, to: Function.self)
807
+ function(target, selector, value)
808
+ }
809
+
810
+ private func invokeDoubleSetter(target: NSObject, selectorName: String, value: Double) {
811
+ let selector = NSSelectorFromString(selectorName)
812
+ guard target.responds(to: selector) else { return }
813
+ typealias Function = @convention(c) (AnyObject, Selector, Double) -> Void
814
+ let implementation = target.method(for: selector)
815
+ let function = unsafeBitCast(implementation, to: Function.self)
816
+ function(target, selector, value)
817
+ }
818
+
819
+ private func invokeIntReturnBoolSetter(target: NSObject, selectorName: String, value: Int) -> Bool? {
820
+ let selector = NSSelectorFromString(selectorName)
821
+ guard target.responds(to: selector) else { return nil }
822
+ typealias Function = @convention(c) (AnyObject, Selector, Int) -> Bool
823
+ let implementation = target.method(for: selector)
824
+ let function = unsafeBitCast(implementation, to: Function.self)
825
+ return function(target, selector, value)
826
+ }
827
+
828
+ private func invokeObjectSetter(target: NSObject, selectorName: String, value: AnyObject) {
829
+ let selector = NSSelectorFromString(selectorName)
830
+ guard target.responds(to: selector) else { return }
831
+ typealias Function = @convention(c) (AnyObject, Selector, AnyObject) -> Void
832
+ let implementation = target.method(for: selector)
833
+ let function = unsafeBitCast(implementation, to: Function.self)
834
+ function(target, selector, value)
835
+ }
836
+
837
+ private func boolValue(_ raw: Any?) -> Bool? {
838
+ if let value = raw as? Bool { return value }
839
+ if let value = raw as? NSNumber { return value.boolValue }
840
+ if let value = raw as? String {
841
+ let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
842
+ if normalized == "true" || normalized == "1" { return true }
843
+ if normalized == "false" || normalized == "0" { return false }
844
+ }
845
+ return nil
846
+ }
847
+
848
+ private func intValue(_ raw: Any?) -> Int? {
849
+ if let value = raw as? Int { return value }
850
+ if let value = raw as? NSNumber { return value.intValue }
851
+ if let value = raw as? String {
852
+ return Int(value.trimmingCharacters(in: .whitespacesAndNewlines))
853
+ }
854
+ return nil
855
+ }
856
+
857
+ private func doubleValue(_ raw: Any?) -> Double? {
858
+ if let value = raw as? Double { return value }
859
+ if let value = raw as? Float { return Double(value) }
860
+ if let value = raw as? NSNumber { return value.doubleValue }
861
+ if let value = raw as? String {
862
+ return Double(value.trimmingCharacters(in: .whitespacesAndNewlines))
863
+ }
864
+ return nil
865
+ }
866
+
867
+ private func nonEmptyString(_ raw: Any?) -> String? {
868
+ guard let value = raw as? String else { return nil }
869
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
870
+ return trimmed.isEmpty ? nil : trimmed
871
+ }
299
872
  }