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
@@ -8,6 +8,7 @@ import com.amap.api.navi.model.AMapNaviPathGroup
8
8
  import expo.modules.kotlin.Promise
9
9
  import expo.modules.kotlin.modules.Module
10
10
  import expo.modules.kotlin.modules.ModuleDefinition
11
+ import expo.modules.gaodemap.map.modules.SDKInitializer
11
12
  import expo.modules.gaodemap.navigation.routes.drive.DriveTruckRouteCalculator
12
13
  import expo.modules.gaodemap.navigation.routes.walkride.WalkRideRouteCalculator
13
14
  import expo.modules.gaodemap.navigation.routes.ebike.EbikeRouteCalculator
@@ -15,6 +16,7 @@ import expo.modules.gaodemap.navigation.listeners.IndependentRouteListener
15
16
  import expo.modules.gaodemap.navigation.utils.Converters
16
17
  import expo.modules.gaodemap.navigation.managers.IndependentRouteManager
17
18
  import expo.modules.gaodemap.navigation.services.IndependentRouteService
19
+ import java.util.Locale
18
20
 
19
21
 
20
22
  /**
@@ -48,6 +50,12 @@ class ExpoGaodeMapNavigationModule : Module() {
48
50
  override fun definition() = ModuleDefinition {
49
51
  Name("ExpoGaodeMapNavigation")
50
52
 
53
+ OnCreate {
54
+ appContext.reactContext?.let { context ->
55
+ SDKInitializer.restorePersistedState(context)
56
+ }
57
+ }
58
+
51
59
  // 仅保留算路相关事件(用于需要事件的场景;Promise 也会在 Listener 中完成)
52
60
  Events(
53
61
  "onCalculateRouteSuccess",
@@ -200,6 +208,7 @@ class ExpoGaodeMapNavigationModule : Module() {
200
208
  */
201
209
  AsyncFunction("startNaviWithIndependentPath") { options: Map<String, Any?>, promise: Promise ->
202
210
  try {
211
+ ensureNavigationPrivacyReady()
203
212
  val token = (options["token"] as? Number)?.toInt() ?: throw Exception("token is required")
204
213
  val routeId = (options["routeId"] as? Number)?.toInt()
205
214
  val routeIndex = (options["routeIndex"] as? Number)?.toInt()
@@ -212,6 +221,29 @@ class ExpoGaodeMapNavigationModule : Module() {
212
221
  }
213
222
  }
214
223
 
224
+ /**
225
+ * 打开高德官方导航页面(AmapNaviPage.showRouteActivity)
226
+ * 参数:
227
+ * - from?: { latitude, longitude, name?, poiId? }(可选,不传则使用当前位置)
228
+ * - to: { latitude, longitude, name?, poiId? }(必填)
229
+ * - waypoints?: Array<{ latitude, longitude, name?, poiId? }>(可选)
230
+ * - needCalculateRouteWhenPresent?: Boolean(默认 true)
231
+ * - needDestroyDriveManagerInstanceWhenNaviExit?: Boolean(默认 false)
232
+ * - showExitNaviDialog?: Boolean(默认 true)
233
+ * - useInnerVoice?: Boolean(默认 true)
234
+ * - pageType?: String(可选,例如 ROUTE/NAVI)
235
+ * - officialNaviType?: String(可选,例如 DRIVER)
236
+ */
237
+ AsyncFunction("openOfficialNaviPage") { options: Map<String, Any?>, promise: Promise ->
238
+ try {
239
+ ensureNavigationPrivacyReady()
240
+ openOfficialNaviPage(options)
241
+ promise.resolve(true)
242
+ } catch (e: Exception) {
243
+ promise.reject("OPEN_OFFICIAL_PAGE_ERROR", e.message, e)
244
+ }
245
+ }
246
+
215
247
  /**
216
248
  * 清除独立路径组
217
249
  * - token: Int
@@ -230,6 +262,7 @@ class ExpoGaodeMapNavigationModule : Module() {
230
262
  // 注意:摩托车是收费能力;务必在算路前设置 carType=11 和排量,否则不生效
231
263
  AsyncFunction("calculateMotorcycleRoute") { options: Map<String, Any?>, promise: Promise ->
232
264
  try {
265
+ ensureNavigationPrivacyReady()
233
266
  // 设置摩托车车辆信息(车牌可选、排量可选)
234
267
  try {
235
268
  val carNumber = options["carNumber"] as? String
@@ -283,6 +316,7 @@ class ExpoGaodeMapNavigationModule : Module() {
283
316
  }
284
317
 
285
318
  private fun ensureDriveTruck(): DriveTruckRouteCalculator {
319
+ ensureNavigationPrivacyReady()
286
320
  if (driveTruckCalculator == null) {
287
321
  driveTruckCalculator = DriveTruckRouteCalculator(context, this)
288
322
  }
@@ -290,6 +324,7 @@ class ExpoGaodeMapNavigationModule : Module() {
290
324
  }
291
325
 
292
326
  private fun ensureWalkRide(): WalkRideRouteCalculator {
327
+ ensureNavigationPrivacyReady()
293
328
  if (walkRideCalculator == null) {
294
329
  walkRideCalculator = WalkRideRouteCalculator(context, this)
295
330
  }
@@ -297,6 +332,7 @@ class ExpoGaodeMapNavigationModule : Module() {
297
332
  }
298
333
 
299
334
  private fun ensureEbike(): EbikeRouteCalculator {
335
+ ensureNavigationPrivacyReady()
300
336
  if (ebikeCalculator == null) {
301
337
  ebikeCalculator = EbikeRouteCalculator(context, this)
302
338
  }
@@ -304,16 +340,364 @@ class ExpoGaodeMapNavigationModule : Module() {
304
340
  }
305
341
 
306
342
  private fun ensureIndependentService(): IndependentRouteService {
343
+ ensureNavigationPrivacyReady()
307
344
  if (independentRouteService == null) {
308
345
  independentRouteService = IndependentRouteService(context, this)
309
346
  }
310
347
  return independentRouteService!!
311
348
  }
312
349
 
350
+ private fun ensureNavigationPrivacyReady() {
351
+ SDKInitializer.restorePersistedState(context)
352
+ if (!SDKInitializer.isPrivacyReady()) {
353
+ throw Exception(
354
+ "隐私协议未完成确认,请先调用 setPrivacyConfig(hasShow/hasContainsPrivacy/hasAgree 均为 true)"
355
+ )
356
+ }
357
+ }
358
+
359
+ private fun openOfficialNaviPage(options: Map<String, Any?>) {
360
+ @Suppress("UNCHECKED_CAST")
361
+ val from = options["from"] as? Map<String, Any?>
362
+ @Suppress("UNCHECKED_CAST")
363
+ val to = options["to"] as? Map<String, Any?> ?: throw Exception("to is required")
364
+ @Suppress("UNCHECKED_CAST")
365
+ val waypointsRaw = options["waypoints"] as? List<Map<String, Any?>>
366
+
367
+ val startPoi = createOfficialPoi(from, "起点")
368
+ val endPoi = createOfficialPoi(to, "终点")
369
+ ?: throw Exception("to is required and must contain latitude/longitude")
370
+ val waypointPois = waypointsRaw
371
+ ?.mapIndexedNotNull { index, item -> createOfficialPoi(item, "途经点${index + 1}") }
372
+ ?: emptyList()
373
+
374
+ val params = createAmapNaviParams(
375
+ startPoi = startPoi,
376
+ endPoi = endPoi,
377
+ waypointPois = waypointPois,
378
+ options = options
379
+ )
380
+ showOfficialNaviPage(params)
381
+ }
382
+
383
+ private fun createOfficialPoi(
384
+ coordinate: Map<String, Any?>?,
385
+ fallbackName: String
386
+ ): Any? {
387
+ if (coordinate == null) return null
388
+
389
+ val latitude = (coordinate["latitude"] as? Number)?.toDouble()
390
+ ?: throw Exception("coordinate.latitude is required")
391
+ val longitude = (coordinate["longitude"] as? Number)?.toDouble()
392
+ ?: throw Exception("coordinate.longitude is required")
393
+ val name = (coordinate["name"] as? String)?.trim()?.takeIf { it.isNotEmpty() } ?: fallbackName
394
+ val poiId = (coordinate["poiId"] as? String).orEmpty()
395
+
396
+ val latLngClass = Class.forName("com.amap.api.maps.model.LatLng")
397
+ val latLng = latLngClass
398
+ .getConstructor(Double::class.javaPrimitiveType, Double::class.javaPrimitiveType)
399
+ .newInstance(latitude, longitude)
400
+
401
+ val poiClass = Class.forName("com.amap.api.maps.model.Poi")
402
+ val candidates = listOf<Array<Any?>>(
403
+ arrayOf(name, latLng, poiId),
404
+ arrayOf(name, latLng),
405
+ )
406
+
407
+ for (args in candidates) {
408
+ instantiateIfMatch(poiClass, args)?.let { return it }
409
+ }
410
+
411
+ throw Exception("当前 SDK 的 Poi 构造函数不兼容")
412
+ }
413
+
414
+ private fun createAmapNaviParams(
415
+ startPoi: Any?,
416
+ endPoi: Any,
417
+ waypointPois: List<Any>,
418
+ options: Map<String, Any?>
419
+ ): Any {
420
+ val paramsClass = Class.forName("com.amap.api.navi.AmapNaviParams")
421
+ val naviTypeClass = loadFirstAvailableClass(
422
+ "com.amap.api.navi.AmapNaviType",
423
+ "com.amap.api.navi.enums.AmapNaviType"
424
+ ) ?: throw Exception("当前 SDK 缺少 AmapNaviType 枚举类")
425
+ val pageTypeClass = loadFirstAvailableClass(
426
+ "com.amap.api.navi.AmapPageType",
427
+ "com.amap.api.navi.enums.AmapPageType"
428
+ )
429
+
430
+ val officialNaviType = resolveEnumConstant(
431
+ enumClass = naviTypeClass,
432
+ preferName = options["officialNaviType"] as? String,
433
+ fallbackNames = listOf("DRIVER", "DRIVE", "CAR")
434
+ )
435
+ val pageType = pageTypeClass?.let {
436
+ resolveEnumConstant(
437
+ enumClass = it,
438
+ preferName = options["pageType"] as? String,
439
+ fallbackNames = listOf("ROUTE", "NAVI")
440
+ )
441
+ }
442
+
443
+ val waypointValue: Any? = if (waypointPois.isEmpty()) null else ArrayList(waypointPois)
444
+ val candidates = mutableListOf<Array<Any?>>(
445
+ arrayOf(startPoi, waypointValue, endPoi, officialNaviType, pageType),
446
+ arrayOf(startPoi, waypointValue, endPoi, officialNaviType),
447
+ arrayOf(startPoi, null, endPoi, officialNaviType, pageType),
448
+ arrayOf(startPoi, null, endPoi, officialNaviType),
449
+ arrayOf(startPoi, endPoi, officialNaviType, pageType),
450
+ arrayOf(startPoi, endPoi, officialNaviType),
451
+ )
452
+
453
+ for (args in candidates) {
454
+ val params = instantiateIfMatch(paramsClass, args) ?: continue
455
+ applyOptionalParamsByReflection(params, options)
456
+ return params
457
+ }
458
+
459
+ throw Exception("当前 SDK 的 AmapNaviParams 构造函数不兼容")
460
+ }
461
+
462
+ private fun applyOptionalParamsByReflection(params: Any, options: Map<String, Any?>) {
463
+ // 保持旧行为:未传时仍沿用默认值
464
+ val needCalculate = options["needCalculateRouteWhenPresent"] as? Boolean ?: true
465
+ val needDestroy = options["needDestroyDriveManagerInstanceWhenNaviExit"] as? Boolean ?: false
466
+ val showExitDialog = options["showExitNaviDialog"] as? Boolean ?: true
467
+ val useInnerVoice = options["useInnerVoice"] as? Boolean ?: true
468
+
469
+ invokeSetterIfExists(params, "setNeedCalculateRouteWhenPresent", needCalculate)
470
+ invokeSetterIfExists(params, "setNeedDestroyDriveManagerInstanceWhenNaviExit", needDestroy)
471
+ invokeSetterIfExists(params, "setShowExitNaviDialog", showExitDialog)
472
+ invokeSetterIfExists(params, "setUseInnerVoice", useInnerVoice)
473
+
474
+ // 不依赖 Context 的参数
475
+ invokeSetterIfExists(params, "setMultipleRouteNaviMode", options["multipleRouteNaviMode"])
476
+ invokeSetterIfExists(params, "setTruckMultipleRouteNaviMode", options["truckMultipleRouteNaviMode"])
477
+ invokeSetterIfExists(params, "setSecondActionVisible", options["secondActionVisible"])
478
+ invokeSetterIfExists(params, "setShowCrossImage", options["showCrossImage"])
479
+ invokeSetterIfExists(params, "setShowRouteStrategyPreferenceView", options["showRouteStrategyPreferenceView"])
480
+ invokeSetterIfExists(params, "setShowVoiceSetings", options["showVoiceSettings"])
481
+ invokeSetterIfExists(params, "setTrafficEnabled", options["trafficEnabled"])
482
+ invokeSetterIfExists(params, "setRouteStrategy", options["routeStrategy"])
483
+ invokeSetterIfExists(params, "setNaviMode", options["naviMode"])
484
+
485
+ // 依赖 Context 的参数
486
+ invokeSetterWithContextIfExists(params, "setDayAndNightMode", options["dayAndNightMode"])
487
+ invokeSetterWithContextIfExists(params, "setBroadcastMode", options["broadcastMode"])
488
+ invokeSetterWithContextIfExists(params, "setCarDirectionMode", options["carDirectionMode"])
489
+ invokeSetterWithContextIfExists(params, "setScaleAutoChangeEnable", options["scaleAutoChangeEnable"])
490
+ invokeSetterWithContextIfExists(params, "showEagleMap", options["showEagleMap"])
491
+
492
+ // 主题(枚举)
493
+ val themeName = options["theme"] as? String
494
+ if (!themeName.isNullOrBlank()) {
495
+ val themeClass = runCatching {
496
+ Class.forName("com.amap.api.navi.AmapNaviTheme")
497
+ }.getOrNull()
498
+ if (themeClass != null) {
499
+ val theme = resolveEnumConstant(
500
+ enumClass = themeClass,
501
+ preferName = themeName,
502
+ fallbackNames = emptyList()
503
+ )
504
+ invokeSetterIfExists(params, "setTheme", theme)
505
+ }
506
+ }
507
+
508
+ // 车辆信息(用于限行/货车)
509
+ @Suppress("UNCHECKED_CAST")
510
+ val carInfoMap = options["carInfo"] as? Map<String, Any?>
511
+ if (carInfoMap != null) {
512
+ val carInfo = buildCarInfo(carInfoMap)
513
+ invokeSetterIfExists(params, "setCarInfo", carInfo)
514
+ }
515
+ }
516
+
517
+ private fun buildCarInfo(options: Map<String, Any?>): AMapCarInfo {
518
+ val carInfo = AMapCarInfo()
519
+
520
+ try {
521
+ val carType = (options["carType"] as? String)?.trim()?.takeIf { it.isNotEmpty() }
522
+ if (carType != null) carInfo.carType = carType
523
+ } catch (_: Exception) {}
524
+ try {
525
+ val carNumber = (options["carNumber"] as? String)?.trim()?.takeIf { it.isNotEmpty() }
526
+ if (carNumber != null) carInfo.setCarNumber(carNumber)
527
+ } catch (_: Exception) {}
528
+ try {
529
+ val restriction = options["restriction"] as? Boolean
530
+ if (restriction != null) carInfo.isRestriction = restriction
531
+ } catch (_: Exception) {}
532
+ try {
533
+ val motorcycleCC = (options["motorcycleCC"] as? Number)?.toInt()
534
+ if (motorcycleCC != null) carInfo.motorcycleCC = motorcycleCC
535
+ } catch (_: Exception) {}
536
+
537
+ // 货车相关可选参数(部分版本 SDK 可能不存在对应 setter,故用反射安全调用)
538
+ invokeSetterIfExists(carInfo, "setVehicleAxis", options["vehicleAxis"])
539
+ invokeSetterIfExists(carInfo, "setVehicleHeight", options["vehicleHeight"])
540
+ invokeSetterIfExists(carInfo, "setVehicleLength", options["vehicleLength"])
541
+ invokeSetterIfExists(carInfo, "setVehicleWidth", options["vehicleWidth"])
542
+ invokeSetterIfExists(carInfo, "setVehicleSize", options["vehicleSize"])
543
+ invokeSetterIfExists(carInfo, "setVehicleLoad", options["vehicleLoad"])
544
+ invokeSetterIfExists(carInfo, "setVehicleWeight", options["vehicleWeight"])
545
+ invokeSetterIfExists(carInfo, "setVehicleLoadSwitch", options["vehicleLoadSwitch"])
546
+
547
+ return carInfo
548
+ }
549
+
550
+ private fun invokeSetterIfExists(target: Any, methodName: String, value: Any?) {
551
+ if (value == null) return
552
+ val methods = target.javaClass.methods.filter { method ->
553
+ method.name == methodName && method.parameterTypes.size == 1
554
+ }
555
+ for (method in methods) {
556
+ val converted = coerceValue(method.parameterTypes[0], value) ?: continue
557
+ try {
558
+ method.invoke(target, converted)
559
+ return
560
+ } catch (_: Exception) {
561
+ // try next overload
562
+ }
563
+ }
564
+ }
565
+
566
+ private fun invokeSetterWithContextIfExists(target: Any, methodName: String, value: Any?) {
567
+ if (value == null) return
568
+ val appCtx = context.applicationContext ?: context
569
+ val methods = target.javaClass.methods.filter { method ->
570
+ method.name == methodName && method.parameterTypes.size == 2
571
+ }
572
+ for (method in methods) {
573
+ val params = method.parameterTypes
574
+ if (!params[0].isAssignableFrom(appCtx.javaClass)) continue
575
+ val converted = coerceValue(params[1], value) ?: continue
576
+ try {
577
+ method.invoke(target, appCtx, converted)
578
+ return
579
+ } catch (_: Exception) {
580
+ // try next overload
581
+ }
582
+ }
583
+ }
584
+
585
+ private fun coerceValue(expectedType: Class<*>, value: Any): Any? {
586
+ val boxed = boxPrimitive(expectedType)
587
+ if (boxed.isAssignableFrom(value.javaClass)) return value
588
+
589
+ return when (boxed) {
590
+ java.lang.Integer::class.java -> (value as? Number)?.toInt()
591
+ java.lang.Long::class.java -> (value as? Number)?.toLong()
592
+ java.lang.Double::class.java -> (value as? Number)?.toDouble()
593
+ java.lang.Float::class.java -> (value as? Number)?.toFloat()
594
+ java.lang.Short::class.java -> (value as? Number)?.toShort()
595
+ java.lang.Byte::class.java -> (value as? Number)?.toByte()
596
+ java.lang.Boolean::class.java -> when (value) {
597
+ is Boolean -> value
598
+ is Number -> value.toInt() != 0
599
+ is String -> value.equals("true", ignoreCase = true) || value == "1"
600
+ else -> null
601
+ }
602
+ java.lang.String::class.java -> value.toString()
603
+ else -> {
604
+ if (boxed.isEnum && value is String) {
605
+ val byName = boxed.enumConstants
606
+ ?.associateBy { (it as Enum<*>).name.uppercase(Locale.ROOT) }
607
+ byName?.get(value.trim().uppercase(Locale.ROOT))
608
+ } else {
609
+ null
610
+ }
611
+ }
612
+ }
613
+ }
614
+
615
+ private fun showOfficialNaviPage(params: Any) {
616
+ val pageClass = Class.forName("com.amap.api.navi.AmapNaviPage")
617
+ val pageInstance = pageClass.getMethod("getInstance").invoke(null)
618
+ val showMethod = pageClass.methods.firstOrNull { method ->
619
+ method.name == "showRouteActivity" && method.parameterTypes.size == 3
620
+ } ?: throw Exception("未找到 showRouteActivity 方法")
621
+
622
+ val appCtx = context.applicationContext ?: context
623
+ showMethod.invoke(pageInstance, appCtx, params, null)
624
+ }
625
+
626
+ private fun resolveEnumConstant(
627
+ enumClass: Class<*>,
628
+ preferName: String?,
629
+ fallbackNames: List<String>
630
+ ): Any {
631
+ val constants = enumClass.enumConstants
632
+ ?: throw Exception("${enumClass.simpleName} is not enum")
633
+ val byName = constants.associateBy { (it as Enum<*>).name.uppercase(Locale.ROOT) }
634
+
635
+ val preferred = preferName?.trim()?.uppercase(Locale.ROOT)
636
+ if (!preferred.isNullOrEmpty()) {
637
+ byName[preferred]?.let { return it }
638
+ }
639
+ for (name in fallbackNames) {
640
+ byName[name.uppercase(Locale.ROOT)]?.let { return it }
641
+ }
642
+ return constants.first()
643
+ }
644
+
645
+ private fun loadFirstAvailableClass(vararg classNames: String): Class<*>? {
646
+ for (name in classNames) {
647
+ val clazz = runCatching { Class.forName(name) }.getOrNull()
648
+ if (clazz != null) return clazz
649
+ }
650
+ return null
651
+ }
652
+
653
+ private fun instantiateIfMatch(clazz: Class<*>, args: Array<Any?>): Any? {
654
+ val constructors = clazz.constructors.filter { constructor ->
655
+ constructor.parameterTypes.size == args.size
656
+ }
657
+
658
+ for (constructor in constructors) {
659
+ if (!areArgsCompatible(constructor.parameterTypes, args)) continue
660
+ try {
661
+ return constructor.newInstance(*args)
662
+ } catch (_: Exception) {
663
+ // try next constructor
664
+ }
665
+ }
666
+ return null
667
+ }
668
+
669
+ private fun areArgsCompatible(paramTypes: Array<Class<*>>, args: Array<Any?>): Boolean {
670
+ if (paramTypes.size != args.size) return false
671
+ for (index in paramTypes.indices) {
672
+ val expected = boxPrimitive(paramTypes[index])
673
+ val actual = args[index]
674
+ if (actual == null) {
675
+ if (expected.isPrimitive) return false
676
+ continue
677
+ }
678
+ if (!expected.isAssignableFrom(actual.javaClass)) return false
679
+ }
680
+ return true
681
+ }
682
+
683
+ private fun boxPrimitive(type: Class<*>): Class<*> {
684
+ return when (type) {
685
+ java.lang.Integer.TYPE -> java.lang.Integer::class.java
686
+ java.lang.Long.TYPE -> java.lang.Long::class.java
687
+ java.lang.Boolean.TYPE -> java.lang.Boolean::class.java
688
+ java.lang.Float.TYPE -> java.lang.Float::class.java
689
+ java.lang.Double.TYPE -> java.lang.Double::class.java
690
+ java.lang.Short.TYPE -> java.lang.Short::class.java
691
+ java.lang.Byte.TYPE -> java.lang.Byte::class.java
692
+ java.lang.Character.TYPE -> java.lang.Character::class.java
693
+ else -> type
694
+ }
695
+ }
696
+
313
697
  /**
314
698
  * 发送事件到 JS 层
315
699
  */
316
700
  internal fun sendEvent(name: String, params: Map<String, Any?>?) {
317
701
  this@ExpoGaodeMapNavigationModule.sendEvent(name, params)
318
702
  }
319
- }
703
+ }
@@ -3,6 +3,8 @@ package expo.modules.gaodemap.navigation.routes.drive
3
3
  import android.content.Context
4
4
  import com.amap.api.navi.AMapNavi
5
5
  import com.amap.api.navi.model.AMapCarInfo
6
+ import com.amap.api.navi.model.NaviLatLng
7
+ import com.amap.api.navi.model.NaviPoi
6
8
  import expo.modules.gaodemap.navigation.ExpoGaodeMapNavigationModule
7
9
  import expo.modules.gaodemap.navigation.listeners.RouteCalculateListener
8
10
  import expo.modules.gaodemap.navigation.utils.Converters
@@ -22,6 +24,10 @@ class DriveTruckRouteCalculator(
22
24
  private val context: Context,
23
25
  private val module: ExpoGaodeMapNavigationModule
24
26
  ) {
27
+ companion object {
28
+ private const val TAG = "DriveTruckRouteCalculator"
29
+ }
30
+
25
31
  private var aMapNavi: AMapNavi? = null
26
32
  private var routeListener: RouteCalculateListener? = null
27
33
  private var isInitialized = false
@@ -97,7 +103,8 @@ class DriveTruckRouteCalculator(
97
103
  val fromPoi = from?.let { Converters.parseNaviPoi(it) }
98
104
  val toPoi = Converters.parseNaviPoi(to)
99
105
  @Suppress("UNCHECKED_CAST")
100
- val waypoints = Converters.parseWaypoints(options["waypoints"] as? List<Map<String, Any?>>)
106
+ val waypointsRaw = options["waypoints"] as? List<Map<String, Any?>>
107
+ val waypoints = Converters.parseWaypoints(waypointsRaw)
101
108
 
102
109
  // 1) 直接传策略值,或 2) 通过 strategyConvert 计算
103
110
  val strategy: Int = if (options.containsKey("strategy")) {
@@ -141,8 +148,25 @@ class DriveTruckRouteCalculator(
141
148
  android.util.Log.d("DriveTruckRouteCalculator", "起点: $fromPoi")
142
149
  android.util.Log.d("DriveTruckRouteCalculator", "终点: $toPoi")
143
150
  android.util.Log.d("DriveTruckRouteCalculator", "策略: $strategy")
144
-
145
- val result = aMapNavi?.calculateDriveRoute(fromPoi, toPoi, waypoints, strategy)
151
+
152
+ val fromLatLng = from?.let { Converters.parseNaviLatLng(it) }
153
+ val toLatLng = Converters.parseNaviLatLng(to)
154
+ val waypointsLatLng = Converters.parseWaypointsLatLng(waypointsRaw)
155
+ val avoidPolygons = Converters.parseAvoidPolygons(options["avoidPolygons"])
156
+ val avoidRoad = (options["avoidRoad"] as? String)?.trim()?.takeIf { it.isNotEmpty() }
157
+
158
+ val result = calculateDriveRouteWithOptionalAvoid(
159
+ fromPoi = fromPoi,
160
+ toPoi = toPoi,
161
+ waypointsPoi = waypoints,
162
+ fromLatLng = fromLatLng,
163
+ toLatLng = toLatLng,
164
+ waypointsLatLng = waypointsLatLng,
165
+ strategy = strategy,
166
+ avoidPolygons = avoidPolygons,
167
+ avoidRoad = avoidRoad
168
+ )
169
+
146
170
  android.util.Log.d("DriveTruckRouteCalculator", "calculateDriveRoute 返回: $result")
147
171
  }
148
172
 
@@ -193,4 +217,156 @@ class DriveTruckRouteCalculator(
193
217
  routeListener = null
194
218
  }
195
219
  }
196
- }
220
+
221
+ /**
222
+ * 兼容不同版本 SDK 的避让参数能力:
223
+ * - 若传了 avoidPolygons / avoidRoad,优先尝试反射调用带避让参数的重载;
224
+ * - 不可用时回退到普通算路,保证兼容性。
225
+ */
226
+ private fun calculateDriveRouteWithOptionalAvoid(
227
+ fromPoi: NaviPoi?,
228
+ toPoi: NaviPoi,
229
+ waypointsPoi: List<NaviPoi>,
230
+ fromLatLng: NaviLatLng?,
231
+ toLatLng: NaviLatLng,
232
+ waypointsLatLng: List<NaviLatLng>,
233
+ strategy: Int,
234
+ avoidPolygons: List<List<NaviLatLng>>,
235
+ avoidRoad: String?
236
+ ): Boolean {
237
+ val navi = aMapNavi ?: return false
238
+ val hasAvoidPolygon = avoidPolygons.isNotEmpty()
239
+ val hasAvoidRoad = !avoidRoad.isNullOrBlank()
240
+
241
+ if (hasAvoidPolygon || hasAvoidRoad) {
242
+ val reflected = invokeDriveRouteWithAvoidByReflection(
243
+ navi = navi,
244
+ fromPoi = fromPoi,
245
+ toPoi = toPoi,
246
+ waypointsPoi = waypointsPoi,
247
+ fromLatLng = fromLatLng,
248
+ toLatLng = toLatLng,
249
+ waypointsLatLng = waypointsLatLng,
250
+ strategy = strategy,
251
+ avoidPolygons = avoidPolygons,
252
+ avoidRoad = avoidRoad.orEmpty()
253
+ )
254
+
255
+ if (reflected != null) {
256
+ return reflected
257
+ }
258
+
259
+ android.util.Log.w(
260
+ TAG,
261
+ "avoidPolygons/avoidRoad 已传入,但当前 SDK 重载不可用,回退到普通算路"
262
+ )
263
+ }
264
+
265
+ return navi.calculateDriveRoute(fromPoi, toPoi, waypointsPoi, strategy)
266
+ }
267
+
268
+ private fun invokeDriveRouteWithAvoidByReflection(
269
+ navi: AMapNavi,
270
+ fromPoi: NaviPoi?,
271
+ toPoi: NaviPoi,
272
+ waypointsPoi: List<NaviPoi>,
273
+ fromLatLng: NaviLatLng?,
274
+ toLatLng: NaviLatLng,
275
+ waypointsLatLng: List<NaviLatLng>,
276
+ strategy: Int,
277
+ avoidPolygons: List<List<NaviLatLng>>,
278
+ avoidRoad: String
279
+ ): Boolean? {
280
+ val startList = fromLatLng?.let { listOf(it) }
281
+ val endList = listOf(toLatLng)
282
+
283
+ val avoidPolygonArgs: List<Any> = buildList {
284
+ if (avoidPolygons.isNotEmpty()) {
285
+ add(avoidPolygons)
286
+ add(avoidPolygons.first())
287
+ add(avoidPolygons.flatten())
288
+ } else {
289
+ add(emptyList<NaviLatLng>())
290
+ }
291
+ }
292
+
293
+ // 依次尝试常见重载:List 版本、POI 版本;以及是否包含 avoidRoad。
294
+ for (avoidArg in avoidPolygonArgs) {
295
+ if (startList != null) {
296
+ invokeBooleanMethodIfMatch(
297
+ target = navi,
298
+ methodName = "calculateDriveRoute",
299
+ args = arrayOf(startList, endList, waypointsLatLng, avoidArg, avoidRoad, strategy)
300
+ )?.let { return it }
301
+
302
+ invokeBooleanMethodIfMatch(
303
+ target = navi,
304
+ methodName = "calculateDriveRoute",
305
+ args = arrayOf(startList, endList, waypointsLatLng, avoidArg, strategy)
306
+ )?.let { return it }
307
+ }
308
+
309
+ if (fromPoi != null) {
310
+ invokeBooleanMethodIfMatch(
311
+ target = navi,
312
+ methodName = "calculateDriveRoute",
313
+ args = arrayOf(fromPoi, toPoi, waypointsPoi, avoidArg, avoidRoad, strategy)
314
+ )?.let { return it }
315
+
316
+ invokeBooleanMethodIfMatch(
317
+ target = navi,
318
+ methodName = "calculateDriveRoute",
319
+ args = arrayOf(fromPoi, toPoi, waypointsPoi, avoidArg, strategy)
320
+ )?.let { return it }
321
+ }
322
+ }
323
+
324
+ return null
325
+ }
326
+
327
+ private fun invokeBooleanMethodIfMatch(
328
+ target: Any,
329
+ methodName: String,
330
+ args: Array<Any>
331
+ ): Boolean? {
332
+ val methods = target.javaClass.methods.filter { method ->
333
+ method.name == methodName && method.parameterTypes.size == args.size
334
+ }
335
+
336
+ for (method in methods) {
337
+ if (!areArgsCompatible(method.parameterTypes, args)) continue
338
+ try {
339
+ val value = method.invoke(target, *args)
340
+ if (value is Boolean) return value
341
+ } catch (_: Exception) {
342
+ // 尝试下一个重载
343
+ }
344
+ }
345
+
346
+ return null
347
+ }
348
+
349
+ private fun areArgsCompatible(paramTypes: Array<Class<*>>, args: Array<Any>): Boolean {
350
+ if (paramTypes.size != args.size) return false
351
+ for (index in paramTypes.indices) {
352
+ val expected = boxPrimitive(paramTypes[index])
353
+ val actual = args[index].javaClass
354
+ if (!expected.isAssignableFrom(actual)) return false
355
+ }
356
+ return true
357
+ }
358
+
359
+ private fun boxPrimitive(type: Class<*>): Class<*> {
360
+ return when (type) {
361
+ java.lang.Integer.TYPE -> java.lang.Integer::class.java
362
+ java.lang.Long.TYPE -> java.lang.Long::class.java
363
+ java.lang.Boolean.TYPE -> java.lang.Boolean::class.java
364
+ java.lang.Float.TYPE -> java.lang.Float::class.java
365
+ java.lang.Double.TYPE -> java.lang.Double::class.java
366
+ java.lang.Short.TYPE -> java.lang.Short::class.java
367
+ java.lang.Byte.TYPE -> java.lang.Byte::class.java
368
+ java.lang.Character.TYPE -> java.lang.Character::class.java
369
+ else -> type
370
+ }
371
+ }
372
+ }