expo-gaode-map-navigation 2.0.12-next.0 → 2.0.13

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 (238) hide show
  1. package/README.md +296 -7
  2. package/android/build.gradle +12 -4
  3. package/android/src/main/AndroidManifest.xml +10 -1
  4. package/android/src/main/cpp/cluster_jni.cpp +56 -0
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +49 -8
  6. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +83 -15
  7. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +13 -3
  8. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +36 -39
  9. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +23 -17
  10. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +5 -2
  11. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +122 -10
  12. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +2 -2
  13. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +37 -25
  14. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +6 -6
  15. package/android/src/main/java/expo/modules/gaodemap/map/search/ExpoGaodeMapSearchModule.kt +751 -0
  16. package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +103 -0
  17. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +1408 -394
  18. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +121 -1
  19. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNavigationModule.kt +14 -28
  20. package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/IndependentRouteListener.kt +28 -3
  21. package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/RouteCalculateListener.kt +2 -2
  22. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/IndependentRouteManager.kt +114 -15
  23. package/android/src/main/java/expo/modules/gaodemap/navigation/routes/drive/DriveTruckRouteCalculator.kt +24 -35
  24. package/android/src/main/java/expo/modules/gaodemap/navigation/services/IndependentRouteService.kt +50 -36
  25. package/android/src/main/java/expo/modules/gaodemap/navigation/services/NavigationForegroundService.kt +661 -0
  26. package/android/src/main/java/expo/modules/gaodemap/navigation/utils/Converters.kt +21 -12
  27. package/android/src/main/res/drawable/ic_nav_notification_small.xml +10 -0
  28. package/android/src/main/res/drawable/landback_0.png +0 -0
  29. package/android/src/main/res/drawable/landback_1.png +0 -0
  30. package/android/src/main/res/drawable/landback_2.png +0 -0
  31. package/android/src/main/res/drawable/landback_3.png +0 -0
  32. package/android/src/main/res/drawable/landback_4.png +0 -0
  33. package/android/src/main/res/drawable/landback_5.png +0 -0
  34. package/android/src/main/res/drawable/landback_6.png +0 -0
  35. package/android/src/main/res/drawable/landback_7.png +0 -0
  36. package/android/src/main/res/drawable/landback_8.png +0 -0
  37. package/android/src/main/res/drawable/landback_9.png +0 -0
  38. package/android/src/main/res/drawable/landback_a.png +0 -0
  39. package/android/src/main/res/drawable/landback_b.png +0 -0
  40. package/android/src/main/res/drawable/landback_c.png +0 -0
  41. package/android/src/main/res/drawable/landback_d.png +0 -0
  42. package/android/src/main/res/drawable/landback_e.png +0 -0
  43. package/android/src/main/res/drawable/landback_f.png +0 -0
  44. package/android/src/main/res/drawable/landback_g.png +0 -0
  45. package/android/src/main/res/drawable/landback_h.png +0 -0
  46. package/android/src/main/res/drawable/landback_i.png +0 -0
  47. package/android/src/main/res/drawable/landback_j.png +0 -0
  48. package/android/src/main/res/drawable/landback_k.png +0 -0
  49. package/android/src/main/res/drawable/landback_l.png +0 -0
  50. package/android/src/main/res/drawable/landfront_0.png +0 -0
  51. package/android/src/main/res/drawable/landfront_00.png +0 -0
  52. package/android/src/main/res/drawable/landfront_1.png +0 -0
  53. package/android/src/main/res/drawable/landfront_11.png +0 -0
  54. package/android/src/main/res/drawable/landfront_20.png +0 -0
  55. package/android/src/main/res/drawable/landfront_21.png +0 -0
  56. package/android/src/main/res/drawable/landfront_22.png +0 -0
  57. package/android/src/main/res/drawable/landfront_3.png +0 -0
  58. package/android/src/main/res/drawable/landfront_33.png +0 -0
  59. package/android/src/main/res/drawable/landfront_40.png +0 -0
  60. package/android/src/main/res/drawable/landfront_43.png +0 -0
  61. package/android/src/main/res/drawable/landfront_44.png +0 -0
  62. package/android/src/main/res/drawable/landfront_5.png +0 -0
  63. package/android/src/main/res/drawable/landfront_55.png +0 -0
  64. package/android/src/main/res/drawable/landfront_61.png +0 -0
  65. package/android/src/main/res/drawable/landfront_63.png +0 -0
  66. package/android/src/main/res/drawable/landfront_66.png +0 -0
  67. package/android/src/main/res/drawable/landfront_70.png +0 -0
  68. package/android/src/main/res/drawable/landfront_71.png +0 -0
  69. package/android/src/main/res/drawable/landfront_73.png +0 -0
  70. package/android/src/main/res/drawable/landfront_77.png +0 -0
  71. package/android/src/main/res/drawable/landfront_8.png +0 -0
  72. package/android/src/main/res/drawable/landfront_88.png +0 -0
  73. package/android/src/main/res/drawable/landfront_90.png +0 -0
  74. package/android/src/main/res/drawable/landfront_95.png +0 -0
  75. package/android/src/main/res/drawable/landfront_99.png +0 -0
  76. package/android/src/main/res/drawable/landfront_a0.png +0 -0
  77. package/android/src/main/res/drawable/landfront_a8.png +0 -0
  78. package/android/src/main/res/drawable/landfront_aa.png +0 -0
  79. package/android/src/main/res/drawable/landfront_b1.png +0 -0
  80. package/android/src/main/res/drawable/landfront_b5.png +0 -0
  81. package/android/src/main/res/drawable/landfront_bb.png +0 -0
  82. package/android/src/main/res/drawable/landfront_c3.png +0 -0
  83. package/android/src/main/res/drawable/landfront_c8.png +0 -0
  84. package/android/src/main/res/drawable/landfront_cc.png +0 -0
  85. package/android/src/main/res/drawable/landfront_d.png +0 -0
  86. package/android/src/main/res/drawable/landfront_dd.png +0 -0
  87. package/android/src/main/res/drawable/landfront_e1.png +0 -0
  88. package/android/src/main/res/drawable/landfront_e5.png +0 -0
  89. package/android/src/main/res/drawable/landfront_ee.png +0 -0
  90. package/android/src/main/res/drawable/landfront_f0.png +0 -0
  91. package/android/src/main/res/drawable/landfront_f1.png +0 -0
  92. package/android/src/main/res/drawable/landfront_f5.png +0 -0
  93. package/android/src/main/res/drawable/landfront_ff.png +0 -0
  94. package/android/src/main/res/drawable/landfront_g3.png +0 -0
  95. package/android/src/main/res/drawable/landfront_g5.png +0 -0
  96. package/android/src/main/res/drawable/landfront_gg.png +0 -0
  97. package/android/src/main/res/drawable/landfront_h1.png +0 -0
  98. package/android/src/main/res/drawable/landfront_h3.png +0 -0
  99. package/android/src/main/res/drawable/landfront_h5.png +0 -0
  100. package/android/src/main/res/drawable/landfront_hh.png +0 -0
  101. package/android/src/main/res/drawable/landfront_i0.png +0 -0
  102. package/android/src/main/res/drawable/landfront_i3.png +0 -0
  103. package/android/src/main/res/drawable/landfront_i5.png +0 -0
  104. package/android/src/main/res/drawable/landfront_ii.png +0 -0
  105. package/android/src/main/res/drawable/landfront_j1.png +0 -0
  106. package/android/src/main/res/drawable/landfront_j8.png +0 -0
  107. package/android/src/main/res/drawable/landfront_jj.png +0 -0
  108. package/android/src/main/res/drawable/landfront_kk.png +0 -0
  109. package/android/src/main/res/drawable/landfront_ll.png +0 -0
  110. package/android/src/main/res/drawable/nav_notification_brand_icon.xml +16 -0
  111. package/android/src/main/res/drawable/navi_arrow_leftline.png +0 -0
  112. package/android/src/main/res/drawable/navi_lane_shape_bg_center.xml +5 -0
  113. package/android/src/main/res/drawable/navi_lane_shape_bg_left.xml +8 -0
  114. package/android/src/main/res/drawable/navi_lane_shape_bg_over.xml +6 -0
  115. package/android/src/main/res/drawable/navi_lane_shape_bg_right.xml +8 -0
  116. package/android/src/main/res/drawable-nodpi/nav_tracker_car.png +0 -0
  117. package/build/ExpoGaodeMapNaviView.d.ts +16 -0
  118. package/build/ExpoGaodeMapNaviView.d.ts.map +1 -1
  119. package/build/ExpoGaodeMapNaviView.js +74 -1
  120. package/build/ExpoGaodeMapNaviView.js.map +1 -1
  121. package/build/index.d.ts +56 -8
  122. package/build/index.d.ts.map +1 -1
  123. package/build/index.js +452 -10
  124. package/build/index.js.map +1 -1
  125. package/build/map/ExpoGaodeMapModule.d.ts +15 -13
  126. package/build/map/ExpoGaodeMapModule.d.ts.map +1 -1
  127. package/build/map/ExpoGaodeMapModule.js +31 -39
  128. package/build/map/ExpoGaodeMapModule.js.map +1 -1
  129. package/build/map/ExpoGaodeMapOfflineModule.d.ts +5 -0
  130. package/build/map/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  131. package/build/map/ExpoGaodeMapOfflineModule.js.map +1 -1
  132. package/build/map/ExpoGaodeMapView.d.ts +3 -4
  133. package/build/map/ExpoGaodeMapView.d.ts.map +1 -1
  134. package/build/map/ExpoGaodeMapView.js +28 -25
  135. package/build/map/ExpoGaodeMapView.js.map +1 -1
  136. package/build/map/components/overlays/Circle.d.ts.map +1 -1
  137. package/build/map/components/overlays/Circle.js +1 -30
  138. package/build/map/components/overlays/Circle.js.map +1 -1
  139. package/build/map/components/overlays/Cluster.d.ts.map +1 -1
  140. package/build/map/components/overlays/Cluster.js +1 -42
  141. package/build/map/components/overlays/Cluster.js.map +1 -1
  142. package/build/map/components/overlays/HeatMap.d.ts.map +1 -1
  143. package/build/map/components/overlays/HeatMap.js +21 -21
  144. package/build/map/components/overlays/HeatMap.js.map +1 -1
  145. package/build/map/components/overlays/Marker.d.ts.map +1 -1
  146. package/build/map/components/overlays/Marker.js +76 -80
  147. package/build/map/components/overlays/Marker.js.map +1 -1
  148. package/build/map/components/overlays/Polygon.d.ts.map +1 -1
  149. package/build/map/components/overlays/Polygon.js +1 -25
  150. package/build/map/components/overlays/Polygon.js.map +1 -1
  151. package/build/map/components/overlays/Polyline.d.ts.map +1 -1
  152. package/build/map/components/overlays/Polyline.js +1 -31
  153. package/build/map/components/overlays/Polyline.js.map +1 -1
  154. package/build/map/index.d.ts +9 -2
  155. package/build/map/index.d.ts.map +1 -1
  156. package/build/map/index.js +9 -2
  157. package/build/map/index.js.map +1 -1
  158. package/build/map/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  159. package/build/map/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  160. package/build/map/search/ExpoGaodeMapSearch.types.js +19 -0
  161. package/build/map/search/ExpoGaodeMapSearch.types.js.map +1 -0
  162. package/build/map/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  163. package/build/map/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  164. package/build/map/search/ExpoGaodeMapSearchModule.js +47 -0
  165. package/build/map/search/ExpoGaodeMapSearchModule.js.map +1 -0
  166. package/build/map/search/index.d.ts +156 -0
  167. package/build/map/search/index.d.ts.map +1 -0
  168. package/build/map/search/index.js +171 -0
  169. package/build/map/search/index.js.map +1 -0
  170. package/build/map/types/index.d.ts +2 -2
  171. package/build/map/types/index.d.ts.map +1 -1
  172. package/build/map/types/index.js.map +1 -1
  173. package/build/map/types/map-view.types.d.ts +4 -2
  174. package/build/map/types/map-view.types.d.ts.map +1 -1
  175. package/build/map/types/map-view.types.js.map +1 -1
  176. package/build/map/types/native-module.types.d.ts +11 -12
  177. package/build/map/types/native-module.types.d.ts.map +1 -1
  178. package/build/map/types/native-module.types.js.map +1 -1
  179. package/build/map/types/overlays.types.d.ts +9 -14
  180. package/build/map/types/overlays.types.d.ts.map +1 -1
  181. package/build/map/types/overlays.types.js.map +1 -1
  182. package/build/map/types/route-playback.types.d.ts +16 -0
  183. package/build/map/types/route-playback.types.d.ts.map +1 -1
  184. package/build/map/types/route-playback.types.js.map +1 -1
  185. package/build/map/utils/ErrorHandler.js +11 -11
  186. package/build/map/utils/ErrorHandler.js.map +1 -1
  187. package/build/map/utils/OfflineMapManager.d.ts +4 -0
  188. package/build/map/utils/OfflineMapManager.d.ts.map +1 -1
  189. package/build/map/utils/OfflineMapManager.js +6 -0
  190. package/build/map/utils/OfflineMapManager.js.map +1 -1
  191. package/build/types/coordinates.types.d.ts +3 -0
  192. package/build/types/coordinates.types.d.ts.map +1 -1
  193. package/build/types/coordinates.types.js.map +1 -1
  194. package/build/types/independent.types.d.ts +111 -12
  195. package/build/types/independent.types.d.ts.map +1 -1
  196. package/build/types/independent.types.js.map +1 -1
  197. package/build/types/native-module.types.d.ts +1 -1
  198. package/build/types/native-module.types.js.map +1 -1
  199. package/build/types/naviview.types.d.ts +304 -14
  200. package/build/types/naviview.types.d.ts.map +1 -1
  201. package/build/types/naviview.types.js.map +1 -1
  202. package/build/types/route.types.d.ts +12 -2
  203. package/build/types/route.types.d.ts.map +1 -1
  204. package/build/types/route.types.js.map +1 -1
  205. package/expo-module.config.json +4 -2
  206. package/ios/ExpoGaodeMapNaviView.swift +2331 -201
  207. package/ios/ExpoGaodeMapNaviViewModule.swift +109 -1
  208. package/ios/ExpoGaodeMapNavigation.podspec +2 -1
  209. package/ios/ExpoGaodeMapNavigationModule.swift +7 -5
  210. package/ios/managers/IndependentRouteManager.swift +90 -26
  211. package/ios/map/ExpoGaodeMapModule.swift +72 -21
  212. package/ios/map/ExpoGaodeMapOfflineModule.swift +61 -0
  213. package/ios/map/ExpoGaodeMapSearchModule.swift +773 -0
  214. package/ios/map/ExpoGaodeMapView.swift +23 -5
  215. package/ios/map/GaodeMapPrivacyManager.swift +26 -18
  216. package/ios/map/cpp/GeometryEngine.cpp +112 -0
  217. package/ios/map/cpp/GeometryEngine.hpp +21 -0
  218. package/ios/map/modules/LocationManager.swift +37 -5
  219. package/ios/map/overlays/MarkerView.swift +11 -11
  220. package/ios/map/overlays/MarkerViewModule.swift +4 -4
  221. package/ios/map/overlays/PolylineView.swift +6 -12
  222. package/ios/map/utils/ClusterNative.h +8 -0
  223. package/ios/map/utils/ClusterNative.mm +27 -0
  224. package/ios/map/utils/PermissionManager.swift +115 -6
  225. package/ios/routes/drive/DriveTruckRouteCalculator.swift +165 -77
  226. package/ios/routes/walkride/WalkRideRouteCalculator.swift +127 -1
  227. package/ios/services/IndependentRouteService.swift +198 -39
  228. package/ios/services/NavigationLiveActivityAttributes.swift +48 -0
  229. package/ios/services/NavigationLiveActivityManager.swift +359 -0
  230. package/package.json +22 -7
  231. package/plugin/build/withGaodeMap.d.ts +8 -0
  232. package/plugin/build/withGaodeMap.js +60 -4
  233. package/scripts/check-expo-modules.js +68 -0
  234. package/shared/cpp/GeometryEngine.cpp +112 -0
  235. package/shared/cpp/GeometryEngine.hpp +21 -0
  236. package/widget-template/README.md +46 -0
  237. package/widget-template/ios/NavigationLiveActivityWidget.swift +367 -0
  238. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/RouteCalculator.kt +0 -173
@@ -8,8 +8,292 @@
8
8
  import Foundation
9
9
  import ExpoModulesCore
10
10
  import AMapNaviKit
11
+ import AVFAudio
12
+ import CoreLocation
13
+
14
+ final class ExpoGaodeMapCustomDriveView: AMapNaviDriveView {
15
+ var suppressLaneInfoUI: Bool = false
16
+ var suppressTopInfoUI: Bool = false {
17
+ didSet {
18
+ scheduleTopInfoSuppressionPasses()
19
+ setNeedsLayout()
20
+ }
21
+ }
22
+ var suppressBottomRightUI: Bool = false {
23
+ didSet {
24
+ scheduleTopInfoSuppressionPasses()
25
+ setNeedsLayout()
26
+ }
27
+ }
28
+ private let topInfoCoverView: UIView = {
29
+ let view = UIView()
30
+ view.isHidden = true
31
+ view.isUserInteractionEnabled = false
32
+ view.backgroundColor = UIColor(red: 14.0 / 255.0, green: 18.0 / 255.0, blue: 26.0 / 255.0, alpha: 0.96)
33
+ return view
34
+ }()
35
+ private var scheduledSuppressionPasses = 0
36
+
37
+ override init(frame: CGRect) {
38
+ super.init(frame: frame)
39
+ installTopInfoCoverViewIfNeeded()
40
+ }
41
+
42
+ required init?(coder: NSCoder) {
43
+ super.init(coder: coder)
44
+ installTopInfoCoverViewIfNeeded()
45
+ }
46
+
47
+ override func layoutSubviews() {
48
+ super.layoutSubviews()
49
+ applySuppressedChromeVisibility()
50
+ }
51
+
52
+ override func didMoveToWindow() {
53
+ super.didMoveToWindow()
54
+ installTopInfoCoverViewIfNeeded()
55
+ if suppressTopInfoUI {
56
+ scheduleTopInfoSuppressionPasses()
57
+ }
58
+ }
59
+
60
+ override func driveManager(_ driveManager: AMapNaviDriveManager, showLaneBackInfo laneBackInfo: String, laneSelectInfo: String) {
61
+ guard !suppressLaneInfoUI else {
62
+ return
63
+ }
64
+ super.driveManager(driveManager, showLaneBackInfo: laneBackInfo, laneSelectInfo: laneSelectInfo)
65
+ }
66
+
67
+ override func driveManagerHideLaneInfo(_ driveManager: AMapNaviDriveManager) {
68
+ guard !suppressLaneInfoUI else {
69
+ return
70
+ }
71
+ super.driveManagerHideLaneInfo(driveManager)
72
+ }
73
+
74
+ private func applySuppressedChromeVisibility() {
75
+ installTopInfoCoverViewIfNeeded()
76
+ let topCandidates = collectTopInfoCandidates()
77
+ for candidate in topCandidates {
78
+ candidate.isHidden = suppressTopInfoUI
79
+ candidate.alpha = suppressTopInfoUI ? 0.0 : 1.0
80
+ }
81
+
82
+ let bottomRightCandidates = collectBottomRightCandidates()
83
+ for candidate in bottomRightCandidates {
84
+ candidate.isHidden = suppressBottomRightUI
85
+ candidate.alpha = suppressBottomRightUI ? 0.0 : 1.0
86
+ }
87
+
88
+ guard suppressTopInfoUI, !topCandidates.isEmpty else {
89
+ topInfoCoverView.isHidden = true
90
+ return
91
+ }
92
+
93
+ let unionFrame = topCandidates.reduce(CGRect.null) { partial, view in
94
+ let frame = view.convert(view.bounds, to: self)
95
+ return partial.union(frame)
96
+ }
97
+ let fallbackFrame = CGRect(x: 0, y: 0, width: bounds.width, height: min(max(bounds.height * 0.17, 96), 150))
98
+ let targetFrame = (unionFrame.isNull ? fallbackFrame : unionFrame.insetBy(dx: -8, dy: -6)).intersection(bounds)
99
+ topInfoCoverView.frame = targetFrame
100
+ topInfoCoverView.isHidden = targetFrame.isEmpty
101
+ if !topInfoCoverView.isHidden {
102
+ bringSubviewToFront(topInfoCoverView)
103
+ }
104
+ }
105
+
106
+ private func collectTopInfoCandidates() -> [UIView] {
107
+ guard suppressTopInfoUI || !subviews.isEmpty else {
108
+ return []
109
+ }
110
+
111
+ let protectedClassNameFragments = [
112
+ "MAMap",
113
+ "Lane",
114
+ "Cross",
115
+ "Eagle",
116
+ "TrafficBar",
117
+ "Compass",
118
+ "Zoom",
119
+ "Scale",
120
+ "Logo",
121
+ ]
122
+
123
+ return allDescendantSubviews(of: self).filter { view in
124
+ guard view !== self, view !== topInfoCoverView else {
125
+ return false
126
+ }
127
+
128
+ let frame = view.convert(view.bounds, to: self)
129
+ guard !frame.isEmpty else {
130
+ return false
131
+ }
132
+
133
+ let className = NSStringFromClass(type(of: view))
134
+ if protectedClassNameFragments.contains(where: { className.localizedCaseInsensitiveContains($0) }) {
135
+ return false
136
+ }
137
+
138
+ let topBandMaxY = min(max(bounds.height * 0.28, 150), 220)
139
+ guard frame.minY <= topBandMaxY && frame.maxY <= topBandMaxY + 70 else {
140
+ return false
141
+ }
142
+
143
+ guard frame.height >= 12 && frame.height <= 140 && frame.width >= 20 else {
144
+ return false
145
+ }
146
+
147
+ // Keep small corner controls (for example compass / map tool buttons) out of the suppression set.
148
+ let isCornerControl =
149
+ frame.width <= 60 &&
150
+ frame.height <= 60 &&
151
+ (frame.minX <= 24 || frame.maxX >= bounds.width - 24)
152
+ if isCornerControl {
153
+ return false
154
+ }
155
+
156
+ return true
157
+ }
158
+ }
159
+
160
+ private func collectBottomRightCandidates() -> [UIView] {
161
+ guard suppressBottomRightUI || !subviews.isEmpty else {
162
+ return []
163
+ }
164
+
165
+ let protectedClassNameFragments = [
166
+ "MAMap",
167
+ "Lane",
168
+ "Cross",
169
+ "Eagle",
170
+ "TrafficBar",
171
+ "Compass",
172
+ "Zoom",
173
+ "Scale",
174
+ "Logo",
175
+ ]
176
+
177
+ return allDescendantSubviews(of: self).filter { view in
178
+ guard view !== self, view !== topInfoCoverView else {
179
+ return false
180
+ }
181
+
182
+ let frame = view.convert(view.bounds, to: self)
183
+ guard !frame.isEmpty else {
184
+ return false
185
+ }
186
+
187
+ let className = NSStringFromClass(type(of: view))
188
+ if protectedClassNameFragments.contains(where: { className.localizedCaseInsensitiveContains($0) }) {
189
+ return false
190
+ }
191
+
192
+ guard frame.minX >= bounds.width * 0.76 || frame.maxX >= bounds.width - 10 else {
193
+ return false
194
+ }
195
+
196
+ guard frame.minY >= bounds.height * 0.46 else {
197
+ return false
198
+ }
199
+
200
+ guard frame.width >= 18 && frame.width <= 110 else {
201
+ return false
202
+ }
203
+
204
+ guard frame.height >= 18 && frame.height <= 240 else {
205
+ return false
206
+ }
207
+
208
+ return true
209
+ }
210
+ }
211
+
212
+ private func allDescendantSubviews(of root: UIView) -> [UIView] {
213
+ root.subviews.flatMap { subview in
214
+ [subview] + allDescendantSubviews(of: subview)
215
+ }
216
+ }
217
+
218
+ private func installTopInfoCoverViewIfNeeded() {
219
+ guard topInfoCoverView.superview !== self else {
220
+ return
221
+ }
222
+ addSubview(topInfoCoverView)
223
+ }
224
+
225
+ private func scheduleTopInfoSuppressionPasses() {
226
+ scheduledSuppressionPasses = (suppressTopInfoUI || suppressBottomRightUI) ? 18 : 0
227
+ guard suppressTopInfoUI || suppressBottomRightUI else {
228
+ topInfoCoverView.isHidden = true
229
+ return
230
+ }
231
+ runScheduledTopInfoSuppressionPass()
232
+ }
233
+
234
+ private func runScheduledTopInfoSuppressionPass() {
235
+ applySuppressedChromeVisibility()
236
+ guard scheduledSuppressionPasses > 0 else {
237
+ return
238
+ }
239
+ scheduledSuppressionPasses -= 1
240
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in
241
+ self?.runScheduledTopInfoSuppressionPass()
242
+ }
243
+ }
244
+
245
+ func refreshSuppressedTopInfoUIIfNeeded() {
246
+ guard suppressTopInfoUI || suppressBottomRightUI else {
247
+ return
248
+ }
249
+ applySuppressedChromeVisibility()
250
+ }
251
+ }
252
+
253
+ private struct NaviCustomWaypointMarkerModel {
254
+ let latitude: Double
255
+ let longitude: Double
256
+ let title: String
257
+ var arrived: Bool = false
258
+ }
259
+
260
+ private final class NaviCustomWaypointBubbleView: UIView {
261
+ init(title: String) {
262
+ let font = UIFont.systemFont(ofSize: 17, weight: .semibold)
263
+ let textWidth = ceil((title as NSString).size(withAttributes: [.font: font]).width)
264
+ let bodyWidth = min(max(textWidth + 28, 62), 110)
265
+ let size = CGSize(width: bodyWidth, height: 34)
266
+ super.init(frame: CGRect(origin: .zero, size: size))
267
+
268
+ backgroundColor = .clear
269
+ isOpaque = false
270
+
271
+ let bodyView = UIView(frame: bounds)
272
+ bodyView.backgroundColor = UIColor(red: 47.0 / 255.0, green: 103.0 / 255.0, blue: 255.0 / 255.0, alpha: 1)
273
+ bodyView.layer.cornerRadius = 17
274
+ bodyView.layer.borderWidth = 2
275
+ bodyView.layer.borderColor = UIColor.white.cgColor
276
+ bodyView.layer.shadowColor = UIColor(red: 21.0 / 255.0, green: 53.0 / 255.0, blue: 127.0 / 255.0, alpha: 1).cgColor
277
+ bodyView.layer.shadowOpacity = 0.18
278
+ bodyView.layer.shadowRadius = 6
279
+ bodyView.layer.shadowOffset = CGSize(width: 0, height: 3)
280
+ addSubview(bodyView)
281
+
282
+ let label = UILabel(frame: bodyView.bounds.insetBy(dx: 10, dy: 4))
283
+ label.text = title
284
+ label.textAlignment = .center
285
+ label.textColor = .white
286
+ label.font = font
287
+ bodyView.addSubview(label)
288
+ }
289
+
290
+ required init?(coder: NSCoder) {
291
+ fatalError("init(coder:) has not been implemented")
292
+ }
293
+ }
11
294
 
12
295
  public class ExpoGaodeMapNaviView: ExpoView {
296
+ private let independentRouteManager = IndependentRouteManager.shared
13
297
 
14
298
  // MARK: - 高德 SDK 初始化检查
15
299
 
@@ -22,7 +306,7 @@ public class ExpoGaodeMapNaviView: ExpoView {
22
306
  code: -1002,
23
307
  userInfo: [
24
308
  NSLocalizedDescriptionKey: "隐私协议未完成确认",
25
- NSLocalizedFailureReasonErrorKey: "请先调用 setPrivacyConfig(或 setPrivacyShow/setPrivacyAgree)并确保参数为 true",
309
+ NSLocalizedFailureReasonErrorKey: "请先调用 setPrivacyConfig 并确保参数为 true",
26
310
  NSLocalizedRecoverySuggestionErrorKey: "建议在首次启动弹窗同意后再进入导航页面。"
27
311
  ]
28
312
  )
@@ -121,15 +405,49 @@ public class ExpoGaodeMapNaviView: ExpoView {
121
405
  let onGpsStatusChanged = EventDispatcher()
122
406
  let onNavigationInfoUpdate = EventDispatcher()
123
407
  let onGpsSignalWeak = EventDispatcher()
408
+ let onNavigationVisualStateUpdate = EventDispatcher()
409
+ let onLaneInfoUpdate = EventDispatcher()
410
+ let onTrafficStatusesUpdate = EventDispatcher()
124
411
 
125
412
  // MARK: - Properties
413
+ private enum NaviScene: String {
414
+ case drive
415
+ case walk
416
+ case ride
417
+ }
418
+
419
+ private var activeScene: NaviScene = .drive
126
420
  private var driveView: AMapNaviDriveView?
127
- private var driveViewLoaded: Bool = false
421
+ private var walkView: AMapNaviWalkView?
422
+ private var rideView: AMapNaviRideView?
128
423
  private var pendingShowUIElements: Bool?
129
- private var pendingShowUIElementsWorkItem: DispatchWorkItem?
130
424
  private var hasStartedNavi: Bool = false
131
425
  private var hasReceivedFirstNaviData: Bool = false
132
426
  private var driveManager: AMapNaviDriveManager?
427
+ private var walkManager: AMapNaviWalkManager?
428
+ private var rideManager: AMapNaviRideManager?
429
+ private var lastKnownSpeed: Int = 0
430
+ private var currentNaviRoute: AMapNaviRoute?
431
+ private var currentRouteTotalLength: Int?
432
+ private var lastNavigationInfoPayload: [String: Any]?
433
+ private var lastTurnIconType: Int?
434
+ private var lastNextTurnIconType: Int?
435
+ private var lastTurnIconImageUri: String?
436
+ private var lastNextTurnIconImageUri: String?
437
+ private var lastTurnIconBase64: String?
438
+ private var trafficBarTotalLength: Int?
439
+ private var isCrossVisible: Bool = false
440
+ private var isLaneInfoVisible: Bool = false
441
+ private var isNavigationAudioSessionActive: Bool = false
442
+ private var hasLoggedMissingBackgroundAudioMode: Bool = false
443
+ private var renderedCustomWaypointAnnotations: [AMapNaviCompositeCustomAnnotation] = []
444
+ private var customWaypointMarkers: [NaviCustomWaypointMarkerModel] = []
445
+ private let customSpeechSynthesizer = AVSpeechSynthesizer()
446
+
447
+ private enum LaneStringKind {
448
+ case background
449
+ case selected
450
+ }
133
451
 
134
452
  // Props - 通用属性
135
453
  var naviType: Int = 0 // 0: GPS, 1: Emulator
@@ -139,6 +457,30 @@ public class ExpoGaodeMapNaviView: ExpoView {
139
457
  var showCamera: Bool = true {
140
458
  didSet { applyShowCamera(showCamera) }
141
459
  }
460
+ var carImageSource: String? {
461
+ didSet { applyCarImageSource() }
462
+ }
463
+ var carImageSize: CGSize? {
464
+ didSet { applyCarImageSource() }
465
+ }
466
+ var carCompassImageSource: String? {
467
+ didSet { applyCarCompassImageSource() }
468
+ }
469
+ var startPointImageSource: String? {
470
+ didSet { applyStartPointImageSource() }
471
+ }
472
+ var wayPointImageSource: String? {
473
+ didSet { applyWayPointImageSource() }
474
+ }
475
+ var customWaypointMarkerPayloads: [[String: Any]]? {
476
+ didSet { applyCustomWaypointMarkerPayloads(customWaypointMarkerPayloads) }
477
+ }
478
+ var endPointImageSource: String? {
479
+ didSet { applyEndPointImageSource() }
480
+ }
481
+ var cameraImageSource: String? {
482
+ didSet { applyCameraImageSource() }
483
+ }
142
484
  var autoLockCar: Bool = true {
143
485
  didSet { applyAutoLockCar(autoLockCar) }
144
486
  }
@@ -166,25 +508,37 @@ public class ExpoGaodeMapNaviView: ExpoView {
166
508
  didSet { driveView?.showRoute = showRoute }
167
509
  }
168
510
  var showTurnArrow: Bool = true {
169
- didSet { driveView?.showTurnArrow = showTurnArrow }
511
+ didSet { applyShowTurnArrow(showTurnArrow) }
170
512
  }
171
513
  var showTrafficBar: Bool = true {
172
514
  didSet { driveView?.showTrafficBar = showTrafficBar }
173
515
  }
516
+ var trafficBarFrame: CGRect = .zero {
517
+ didSet { applyTrafficBarFrame(trafficBarFrame) }
518
+ }
519
+ var trafficBarColors: [String: Any]? {
520
+ didSet { applyTrafficBarColors(trafficBarColors) }
521
+ }
174
522
  var showBrowseRouteButton: Bool = true {
175
- didSet { driveView?.showBrowseRouteButton = showBrowseRouteButton }
523
+ didSet { applyShowBrowseRouteButton(showBrowseRouteButton) }
176
524
  }
177
525
  var showMoreButton: Bool = true {
178
- didSet { driveView?.showMoreButton = showMoreButton }
526
+ didSet { applyShowMoreButton(showMoreButton) }
179
527
  }
180
528
  var showTrafficButton: Bool = true {
181
529
  didSet { driveView?.showTrafficButton = showTrafficButton }
182
530
  }
531
+ var showBackupRoute: Bool = true {
532
+ didSet { driveView?.showBackupRoute = showBackupRoute }
533
+ }
534
+ var showEagleMap: Bool = false {
535
+ didSet { driveView?.showEagleMap = showEagleMap }
536
+ }
183
537
  var showUIElements: Bool = true {
184
- didSet { applyShowUIElementsToDriveViewIfReady() }
538
+ didSet { applyShowUIElementsToActiveViewIfReady() }
185
539
  }
186
- var showGreyAfterPass: Bool = false {
187
- didSet { driveView?.showGreyAfterPass = showGreyAfterPass }
540
+ var showGreyAfterPass: Bool = true {
541
+ didSet { applyShowGreyAfterPass(showGreyAfterPass) }
188
542
  }
189
543
  var showVectorline: Bool = true {
190
544
  didSet { driveView?.showVectorline = showVectorline }
@@ -192,11 +546,51 @@ public class ExpoGaodeMapNaviView: ExpoView {
192
546
  var showTrafficLights: Bool = true {
193
547
  didSet { driveView?.showTrafficLights = showTrafficLights }
194
548
  }
549
+ var showCompassEnabled: Bool? {
550
+ didSet {
551
+ guard let showCompassEnabled else { return }
552
+ applyShowCompassEnabled(showCompassEnabled)
553
+ }
554
+ }
555
+ var showDriveCongestion: Bool = true {
556
+ didSet { driveView?.showDriveCongestion = showDriveCongestion }
557
+ }
558
+ var showTrafficLightView: Bool = true {
559
+ didSet { driveView?.showTrafficLightView = showTrafficLightView }
560
+ }
195
561
  var mapViewModeType: Int = 0 {
196
562
  didSet { applyMapViewModeType(mapViewModeType) }
197
563
  }
198
564
  var lineWidth: CGFloat = 0 {
199
- didSet { driveView?.lineWidth = lineWidth }
565
+ didSet { applyLineWidth(lineWidth) }
566
+ }
567
+ var driveViewEdgePadding: UIEdgeInsets = .zero {
568
+ didSet {
569
+ currentNaviView()?.setNeedsLayout()
570
+ currentNaviView()?.layoutIfNeeded()
571
+ scheduleOverviewRouteVisibleRegionRefresh()
572
+ }
573
+ }
574
+ var screenAnchor: CGPoint = .zero {
575
+ didSet {
576
+ applyScreenAnchor(screenAnchor)
577
+ scheduleOverviewRouteVisibleRegionRefresh()
578
+ }
579
+ }
580
+ var hideNativeTopInfoLayout: Bool = false {
581
+ didSet { applyHideNativeTopInfoLayout(hideNativeTopInfoLayout) }
582
+ }
583
+ var hideNativeLaneInfoLayout: Bool = false {
584
+ didSet { applyHideNativeLaneInfoLayout(hideNativeLaneInfoLayout) }
585
+ }
586
+ var iosLiveActivityEnabled: Bool = false {
587
+ didSet {
588
+ if iosLiveActivityEnabled {
589
+ syncNavigationLiveActivityWithLastPayload()
590
+ } else {
591
+ NavigationLiveActivityManager.shared.stop()
592
+ }
593
+ }
200
594
  }
201
595
 
202
596
  func applyShowUIElements(_ visible: Bool) {
@@ -235,172 +629,1575 @@ public class ExpoGaodeMapNaviView: ExpoView {
235
629
  }
236
630
  return
237
631
  }
238
-
239
- // 初始化驾车导航管理器
240
- driveManager = AMapNaviDriveManager.sharedInstance()
241
- driveManager?.delegate = self
242
-
243
- // 使用内置语音
244
- driveManager?.isUseInternalTTS = true
245
-
246
- // 初始化导航视图
247
- driveView = AMapNaviDriveView(frame: bounds)
248
- driveView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
249
- driveView?.delegate = self
250
-
251
- if let view = driveView {
252
- addSubview(view)
253
- driveManager?.addDataRepresentative(view)
254
- }
255
-
256
- // 应用初始配置
257
- applyViewOptions()
632
+
633
+ switchToScene(.drive)
258
634
  }
259
-
260
- private func applyViewOptions() {
261
- // 通用属性
262
- driveView?.showCamera = showCamera
263
- driveView?.autoSwitchShowModeToCarPositionLocked = autoLockCar
264
- driveView?.autoZoomMapLevel = autoChangeZoom
265
- driveView?.showTrafficLayer = trafficLayerEnabled
266
- driveView?.showCrossImage = realCrossDisplay
267
- driveView?.trackingMode = naviMode == 0 ? .carNorth : .mapNorth
268
-
269
- // iOS 特有属性
270
- driveView?.showRoute = showRoute
271
- driveView?.showTurnArrow = showTurnArrow
272
- driveView?.showTrafficBar = showTrafficBar
273
- driveView?.showBrowseRouteButton = showBrowseRouteButton
274
- driveView?.showMoreButton = showMoreButton
275
- driveView?.showTrafficButton = showTrafficButton
276
- driveView?.showGreyAfterPass = showGreyAfterPass
277
- driveView?.showVectorline = showVectorline
278
- driveView?.showTrafficLights = showTrafficLights
279
- if lineWidth > 0 {
280
- driveView?.lineWidth = lineWidth
635
+
636
+ private func currentNaviView() -> UIView? {
637
+ switch activeScene {
638
+ case .drive:
639
+ return driveView
640
+ case .walk:
641
+ return walkView
642
+ case .ride:
643
+ return rideView
281
644
  }
645
+ }
282
646
 
283
- applyShowUIElementsToDriveViewIfReady()
647
+ private func currentNaviManager() -> AMapNaviBaseManager? {
648
+ switch activeScene {
649
+ case .drive:
650
+ return driveManager
651
+ case .walk:
652
+ return walkManager
653
+ case .ride:
654
+ return rideManager
655
+ }
284
656
  }
285
657
 
286
- private func applyShowUIElementsToDriveViewIfReady() {
287
- guard driveView != nil else {
288
- pendingShowUIElements = showUIElements
658
+ private func setupDriveViewIfNeeded() {
659
+ guard driveView == nil else {
289
660
  return
290
661
  }
662
+ let customDriveView = ExpoGaodeMapCustomDriveView(frame: bounds)
663
+ customDriveView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
664
+ customDriveView.delegate = self
665
+ customDriveView.suppressTopInfoUI = hideNativeTopInfoLayout
666
+ customDriveView.suppressLaneInfoUI = hideNativeLaneInfoLayout
667
+ driveView = customDriveView
668
+ }
291
669
 
292
- guard driveViewLoaded else {
293
- pendingShowUIElements = showUIElements
670
+ private func setupWalkViewIfNeeded() {
671
+ guard walkView == nil else {
294
672
  return
295
673
  }
674
+ let view = AMapNaviWalkView(frame: bounds)
675
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
676
+ view.delegate = self
677
+ walkView = view
678
+ }
296
679
 
297
- let value = pendingShowUIElements ?? showUIElements
298
- pendingShowUIElements = nil
680
+ private func setupRideViewIfNeeded() {
681
+ guard rideView == nil else {
682
+ return
683
+ }
684
+ let view = AMapNaviRideView(frame: bounds)
685
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
686
+ view.delegate = self
687
+ rideView = view
688
+ }
299
689
 
300
- if value == false && (!hasStartedNavi || !hasReceivedFirstNaviData) {
301
- pendingShowUIElements = false
302
- pendingShowUIElementsWorkItem?.cancel()
303
- let workItem = DispatchWorkItem { [weak self] in
304
- guard let self else { return }
305
- self.applyDriveViewShowUIElements(true, remainingAttempts: 20)
690
+ private func attachActiveView(_ targetView: UIView?) {
691
+ let knownViews = [driveView, walkView, rideView].compactMap { $0 }
692
+ for view in knownViews where view !== targetView {
693
+ if view.superview === self {
694
+ view.removeFromSuperview()
306
695
  }
307
- pendingShowUIElementsWorkItem = workItem
308
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.08, execute: workItem)
696
+ }
697
+
698
+ guard let targetView else {
309
699
  return
310
700
  }
701
+ targetView.frame = bounds
702
+ if targetView.superview !== self {
703
+ addSubview(targetView)
704
+ }
705
+ sendSubviewToBack(targetView)
706
+ }
707
+
708
+ private func switchToScene(_ scene: NaviScene) {
709
+ teardownManagers(except: scene)
710
+ activeScene = scene
311
711
 
312
- pendingShowUIElementsWorkItem?.cancel()
313
- let workItem = DispatchWorkItem { [weak self] in
314
- guard let self else { return }
315
- self.applyDriveViewShowUIElements(value, remainingAttempts: 20)
712
+ switch scene {
713
+ case .drive:
714
+ setupDriveViewIfNeeded()
715
+ rebindDriveManagerToView()
716
+ attachActiveView(driveView)
717
+ case .walk:
718
+ setupWalkViewIfNeeded()
719
+ rebindWalkManagerToView()
720
+ attachActiveView(walkView)
721
+ case .ride:
722
+ setupRideViewIfNeeded()
723
+ rebindRideManagerToView()
724
+ attachActiveView(rideView)
316
725
  }
317
- pendingShowUIElementsWorkItem = workItem
318
726
 
319
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.08, execute: workItem)
727
+ applyViewOptions()
728
+ applyShowUIElementsToActiveViewIfReady()
729
+ DispatchQueue.main.async { [weak self] in
730
+ self?.onNavigationReady([:])
731
+ }
320
732
  }
321
733
 
322
- private func applyDriveViewShowUIElements(_ value: Bool, remainingAttempts: Int) {
323
- guard let driveView else { return }
734
+ private func teardownManagers(except scene: NaviScene) {
735
+ if scene != .drive {
736
+ destroyDriveManagerIfNeeded()
737
+ }
738
+ if scene != .walk {
739
+ destroyWalkManagerIfNeeded()
740
+ }
741
+ if scene != .ride {
742
+ destroyRideManagerIfNeeded()
743
+ }
744
+ }
324
745
 
325
- if driveView.bounds.isEmpty {
326
- if remainingAttempts <= 0 {
327
- return
328
- }
329
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
330
- self?.applyDriveViewShowUIElements(value, remainingAttempts: remainingAttempts - 1)
331
- }
746
+ private func destroyDriveManagerIfNeeded() {
747
+ guard let driveManager else {
332
748
  return
333
749
  }
334
-
335
- driveView.showUIElements = value
750
+ if let view = driveView {
751
+ driveManager.removeDataRepresentative(view)
752
+ }
753
+ driveManager.removeDataRepresentative(self)
754
+ driveManager.delegate = nil
755
+ self.driveManager = nil
756
+ _ = AMapNaviDriveManager.destroyInstance()
336
757
  }
337
758
 
338
- // MARK: - Prop Setters
339
- private func applyShowCamera(_ show: Bool) {
340
- driveView?.showCamera = show
759
+ private func destroyWalkManagerIfNeeded() {
760
+ guard let walkManager else {
761
+ return
762
+ }
763
+ if let view = walkView {
764
+ walkManager.removeDataRepresentative(view)
765
+ }
766
+ walkManager.removeDataRepresentative(self)
767
+ walkManager.delegate = nil
768
+ self.walkManager = nil
769
+ _ = AMapNaviWalkManager.destroyInstance()
341
770
  }
342
-
343
- private func applyEnableVoice(_ enabled: Bool) {
344
- driveManager?.isUseInternalTTS = enabled
345
- // 内置语音会自动播报,不需要手动调用 startSpeak/stopSpeak
771
+
772
+ private func destroyRideManagerIfNeeded() {
773
+ guard let rideManager else {
774
+ return
775
+ }
776
+ if let view = rideView {
777
+ rideManager.removeDataRepresentative(view)
778
+ }
779
+ rideManager.removeDataRepresentative(self)
780
+ rideManager.delegate = nil
781
+ self.rideManager = nil
782
+ _ = AMapNaviRideManager.destroyInstance()
346
783
  }
347
-
348
- private func applyAutoLockCar(_ enabled: Bool) {
349
- driveView?.autoSwitchShowModeToCarPositionLocked = enabled
784
+
785
+ private func rebindDriveManagerToView() {
786
+ driveManager = AMapNaviDriveManager.sharedInstance()
787
+ driveManager?.delegate = self
788
+ applyDriveManagerBackgroundLocationOptionsIfNeeded()
789
+ driveManager?.isUseInternalTTS = enableVoice
790
+ driveManager?.removeDataRepresentative(self)
791
+ if let view = driveView {
792
+ driveManager?.removeDataRepresentative(view)
793
+ driveManager?.addDataRepresentative(view)
794
+ }
795
+ driveManager?.addDataRepresentative(self)
350
796
  }
351
-
352
- private func applyAutoChangeZoom(_ enabled: Bool) {
353
- driveView?.autoZoomMapLevel = enabled
797
+
798
+ private func rebindWalkManagerToView() {
799
+ walkManager = AMapNaviWalkManager.sharedInstance()
800
+ walkManager?.delegate = self
801
+ applyWalkManagerBackgroundLocationOptionsIfNeeded()
802
+ walkManager?.isUseInternalTTS = enableVoice
803
+ walkManager?.removeDataRepresentative(self)
804
+ if let view = walkView {
805
+ walkManager?.removeDataRepresentative(view)
806
+ walkManager?.addDataRepresentative(view)
807
+ }
808
+ walkManager?.addDataRepresentative(self)
354
809
  }
355
-
356
- private func applyTrafficLayerEnabled(_ enabled: Bool) {
357
- driveView?.showTrafficLayer = enabled
810
+
811
+ private func rebindRideManagerToView() {
812
+ rideManager = AMapNaviRideManager.sharedInstance()
813
+ rideManager?.delegate = self
814
+ applyRideManagerBackgroundLocationOptionsIfNeeded()
815
+ rideManager?.isUseInternalTTS = enableVoice
816
+ rideManager?.removeDataRepresentative(self)
817
+ if let view = rideView {
818
+ rideManager?.removeDataRepresentative(view)
819
+ rideManager?.addDataRepresentative(view)
820
+ }
821
+ rideManager?.addDataRepresentative(self)
358
822
  }
359
-
360
- private func applyRealCrossDisplay(_ enabled: Bool) {
361
- driveView?.showCrossImage = enabled
823
+
824
+ private func applyDriveManagerBackgroundLocationOptionsIfNeeded() {
825
+ guard let driveManager else {
826
+ return
827
+ }
828
+ driveManager.pausesLocationUpdatesAutomatically = false
829
+ let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
830
+ if backgroundModes?.contains("location") == true {
831
+ driveManager.allowsBackgroundLocationUpdates = true
832
+ }
362
833
  }
363
-
364
- private func applyNaviMode(_ mode: Int) {
365
- driveView?.trackingMode = mode == 0 ? .carNorth : .mapNorth
834
+
835
+ private func applyWalkManagerBackgroundLocationOptionsIfNeeded() {
836
+ guard let walkManager else {
837
+ return
838
+ }
839
+ walkManager.pausesLocationUpdatesAutomatically = false
840
+ let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
841
+ if backgroundModes?.contains("location") == true {
842
+ walkManager.allowsBackgroundLocationUpdates = true
843
+ }
366
844
  }
367
-
368
- private func applyShowMode(_ mode: Int) {
369
- // 1: 锁车态, 2: 全览态, 3: 普通态
370
- switch mode {
371
- case 1:
372
- driveView?.showMode = .carPositionLocked
373
- case 2:
374
- driveView?.showMode = .overview
375
- case 3:
376
- driveView?.showMode = .normal
377
- default:
378
- break
845
+
846
+ private func applyRideManagerBackgroundLocationOptionsIfNeeded() {
847
+ guard let rideManager else {
848
+ return
849
+ }
850
+ rideManager.pausesLocationUpdatesAutomatically = false
851
+ let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
852
+ if backgroundModes?.contains("location") == true {
853
+ rideManager.allowsBackgroundLocationUpdates = true
379
854
  }
380
855
  }
381
-
382
- private func applyNightMode(_ enabled: Bool) {
383
- driveView?.mapViewModeType = enabled ? .night : .day
856
+
857
+ private func hasBackgroundAudioModeEnabled() -> Bool {
858
+ let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
859
+ return backgroundModes?.contains("audio") == true
384
860
  }
385
-
386
- private func applyMapViewModeType(_ type: Int) {
387
- // 0: 白天, 1: 黑夜, 2: 自动切换, 3: 自定义
388
- switch type {
389
- case 0:
390
- driveView?.mapViewModeType = .day
391
- case 1:
392
- driveView?.mapViewModeType = .night
393
- case 2:
394
- driveView?.mapViewModeType = .dayNightAuto
395
- case 3:
396
- driveView?.mapViewModeType = .custom
397
- default:
398
- break
861
+
862
+ private func activateNavigationAudioSessionIfNeeded(reason: String) {
863
+ guard enableVoice else {
864
+ return
399
865
  }
400
- }
401
-
402
- // MARK: - Public Methods
403
- func startNavigation(startLat: Double, startLng: Double, endLat: Double, endLng: Double, promise: Promise) {
866
+
867
+ if !hasBackgroundAudioModeEnabled(), !hasLoggedMissingBackgroundAudioMode {
868
+ hasLoggedMissingBackgroundAudioMode = true
869
+ NSLog(
870
+ "[ExpoGaodeMapNaviView][Audio] UIBackgroundModes 缺少 audio,切后台后语音可能中断。reason=%@",
871
+ reason
872
+ )
873
+ }
874
+
875
+ let audioSession = AVAudioSession.sharedInstance()
876
+ do {
877
+ try audioSession.setCategory(
878
+ .playback,
879
+ mode: .voicePrompt,
880
+ options: [.duckOthers, .allowBluetooth, .allowBluetoothA2DP]
881
+ )
882
+ try audioSession.setActive(true)
883
+ isNavigationAudioSessionActive = true
884
+ NSLog("[ExpoGaodeMapNaviView][Audio] activated. reason=%@", reason)
885
+ } catch {
886
+ NSLog(
887
+ "[ExpoGaodeMapNaviView][Audio] activate failed. reason=%@ error=%@",
888
+ reason,
889
+ String(describing: error)
890
+ )
891
+ }
892
+ }
893
+
894
+ private func deactivateNavigationAudioSessionIfNeeded(reason: String) {
895
+ guard isNavigationAudioSessionActive else {
896
+ return
897
+ }
898
+ let audioSession = AVAudioSession.sharedInstance()
899
+ do {
900
+ try audioSession.setActive(false, options: [.notifyOthersOnDeactivation])
901
+ isNavigationAudioSessionActive = false
902
+ NSLog("[ExpoGaodeMapNaviView][Audio] deactivated. reason=%@", reason)
903
+ } catch {
904
+ NSLog(
905
+ "[ExpoGaodeMapNaviView][Audio] deactivate failed. reason=%@ error=%@",
906
+ reason,
907
+ String(describing: error)
908
+ )
909
+ }
910
+ }
911
+
912
+ private func resetTransientNavigationState() {
913
+ hasStartedNavi = false
914
+ hasReceivedFirstNaviData = false
915
+ lastKnownSpeed = 0
916
+ currentNaviRoute = nil
917
+ currentRouteTotalLength = nil
918
+ trafficBarTotalLength = nil
919
+ lastNavigationInfoPayload = nil
920
+ lastTurnIconType = nil
921
+ lastNextTurnIconType = nil
922
+ lastTurnIconBase64 = nil
923
+ clearCachedTurnIconUris()
924
+ isCrossVisible = false
925
+ isLaneInfoVisible = false
926
+ resetCustomWaypointArrivalState()
927
+ emitVisualStateUpdate()
928
+ NavigationLiveActivityManager.shared.stop()
929
+ deactivateNavigationAudioSessionIfNeeded(reason: "reset_transient_navigation_state")
930
+ }
931
+
932
+ private func emitVisualStateUpdate() {
933
+ onNavigationVisualStateUpdate([
934
+ "isCrossVisible": isCrossVisible,
935
+ // iOS 官方导航 SDK 公开的是 showCrossImage / hideCrossImage,
936
+ // 当前没有 Android showModeCross / hideModeCross 对等的 3D 路口模型接口。
937
+ "isModeCrossVisible": false,
938
+ "isLaneInfoVisible": isLaneInfoVisible
939
+ ])
940
+ }
941
+
942
+ private func emitNavigationInfoUpdate(_ payload: [String: Any]) {
943
+ (driveView as? ExpoGaodeMapCustomDriveView)?.refreshSuppressedTopInfoUIIfNeeded()
944
+ var nextPayload = payload
945
+ let payloadIconType = payloadIntValue(nextPayload["iconType"]) ?? 0
946
+ if payloadIconType <= 0, let lastTurnIconType {
947
+ nextPayload["iconType"] = lastTurnIconType
948
+ nextPayload["iconDirection"] = lastTurnIconType
949
+ }
950
+ let payloadNextIconType = payloadIntValue(nextPayload["nextIconType"]) ?? 0
951
+ if payloadNextIconType <= 0, let lastNextTurnIconType {
952
+ nextPayload["nextIconType"] = lastNextTurnIconType
953
+ }
954
+ if let lastTurnIconImageUri {
955
+ nextPayload["turnIconImage"] = lastTurnIconImageUri
956
+ } else {
957
+ nextPayload.removeValue(forKey: "turnIconImage")
958
+ }
959
+ if let lastNextTurnIconImageUri {
960
+ nextPayload["nextTurnIconImage"] = lastNextTurnIconImageUri
961
+ } else {
962
+ nextPayload.removeValue(forKey: "nextTurnIconImage")
963
+ }
964
+ lastNavigationInfoPayload = nextPayload
965
+ onNavigationInfoUpdate(nextPayload)
966
+ syncNavigationLiveActivity(payload: nextPayload)
967
+ }
968
+
969
+ private func reemitLastNavigationInfoIfNeeded() {
970
+ guard var nextPayload = lastNavigationInfoPayload else {
971
+ return
972
+ }
973
+ if let lastTurnIconType {
974
+ nextPayload["iconType"] = lastTurnIconType
975
+ nextPayload["iconDirection"] = lastTurnIconType
976
+ }
977
+ if let lastNextTurnIconType {
978
+ nextPayload["nextIconType"] = lastNextTurnIconType
979
+ } else {
980
+ nextPayload.removeValue(forKey: "nextIconType")
981
+ }
982
+ if let lastTurnIconImageUri {
983
+ nextPayload["turnIconImage"] = lastTurnIconImageUri
984
+ } else {
985
+ nextPayload.removeValue(forKey: "turnIconImage")
986
+ }
987
+ if let lastNextTurnIconImageUri {
988
+ nextPayload["nextTurnIconImage"] = lastNextTurnIconImageUri
989
+ } else {
990
+ nextPayload.removeValue(forKey: "nextTurnIconImage")
991
+ }
992
+
993
+ lastNavigationInfoPayload = nextPayload
994
+ onNavigationInfoUpdate(nextPayload)
995
+ syncNavigationLiveActivity(payload: nextPayload)
996
+ }
997
+
998
+ private func resolveAppDisplayName() -> String {
999
+ let info = Bundle.main.infoDictionary
1000
+ if let displayName = info?["CFBundleDisplayName"] as? String, !displayName.isEmpty {
1001
+ return displayName
1002
+ }
1003
+ if let appName = info?["CFBundleName"] as? String, !appName.isEmpty {
1004
+ return appName
1005
+ }
1006
+ return "导航"
1007
+ }
1008
+
1009
+ private func payloadIntValue(_ value: Any?) -> Int? {
1010
+ if let intValue = value as? Int {
1011
+ return intValue
1012
+ }
1013
+ if let numberValue = value as? NSNumber {
1014
+ return numberValue.intValue
1015
+ }
1016
+ if let doubleValue = value as? Double {
1017
+ return Int(doubleValue)
1018
+ }
1019
+ if let floatValue = value as? Float {
1020
+ return Int(floatValue)
1021
+ }
1022
+ return nil
1023
+ }
1024
+
1025
+ private func makeLiveActivitySnapshot(payload: [String: Any]?) -> NavigationLiveActivitySnapshot? {
1026
+ guard let payload else {
1027
+ return nil
1028
+ }
1029
+
1030
+ let remainDistance = payloadIntValue(payload["pathRetainDistance"]) ?? 0
1031
+ let routeTotalDistance = max(
1032
+ trafficBarTotalLength ?? 0,
1033
+ currentRouteTotalLength ?? 0,
1034
+ remainDistance
1035
+ )
1036
+ let remainTime = payloadIntValue(payload["pathRetainTime"]) ?? 0
1037
+ let stepRemainDistance = payloadIntValue(payload["curStepRetainDistance"]) ?? 0
1038
+ let iconType = payloadIntValue(payload["iconType"]) ?? 0
1039
+ let currentRoadName = (payload["currentRoadName"] as? String) ?? ""
1040
+ let nextRoadName = (payload["nextRoadName"] as? String) ?? ""
1041
+
1042
+ return NavigationLiveActivitySnapshot(
1043
+ appName: resolveAppDisplayName(),
1044
+ currentRoadName: currentRoadName,
1045
+ nextRoadName: nextRoadName,
1046
+ pathRetainDistance: remainDistance,
1047
+ routeTotalDistance: routeTotalDistance,
1048
+ pathRetainTime: remainTime,
1049
+ curStepRetainDistance: stepRemainDistance,
1050
+ iconType: iconType,
1051
+ turnIconBase64: lastTurnIconBase64
1052
+ )
1053
+ }
1054
+
1055
+ private func syncNavigationLiveActivity(payload: [String: Any]?) {
1056
+ guard iosLiveActivityEnabled else {
1057
+ return
1058
+ }
1059
+ guard hasStartedNavi else {
1060
+ return
1061
+ }
1062
+
1063
+ guard let snapshot = makeLiveActivitySnapshot(payload: payload) else {
1064
+ return
1065
+ }
1066
+ NavigationLiveActivityManager.shared.startOrUpdate(snapshot: snapshot)
1067
+ }
1068
+
1069
+ private func syncNavigationLiveActivityWithLastPayload() {
1070
+ syncNavigationLiveActivity(payload: lastNavigationInfoPayload)
1071
+ }
1072
+
1073
+ private func normalizedTurnIconImage(_ image: UIImage) -> UIImage {
1074
+ guard image.imageOrientation != .up else {
1075
+ return image
1076
+ }
1077
+ let renderer = UIGraphicsImageRenderer(size: image.size)
1078
+ return renderer.image { _ in
1079
+ image.draw(in: CGRect(origin: .zero, size: image.size))
1080
+ }
1081
+ }
1082
+
1083
+ private func trimmedTransparentBoundsImage(_ image: UIImage) -> UIImage {
1084
+ guard let cgImage = image.cgImage else {
1085
+ return image
1086
+ }
1087
+ guard let dataProvider = cgImage.dataProvider, let data = dataProvider.data else {
1088
+ return image
1089
+ }
1090
+
1091
+ let bytes = CFDataGetBytePtr(data)
1092
+ let bytesPerPixel = max(cgImage.bitsPerPixel / 8, 0)
1093
+ let bytesPerRow = cgImage.bytesPerRow
1094
+ let width = cgImage.width
1095
+ let height = cgImage.height
1096
+ guard let bytes, bytesPerPixel >= 4, width > 0, height > 0 else {
1097
+ return image
1098
+ }
1099
+
1100
+ let alphaOffset: Int
1101
+ switch cgImage.alphaInfo {
1102
+ case .premultipliedLast, .last, .noneSkipLast:
1103
+ alphaOffset = 3
1104
+ case .premultipliedFirst, .first, .noneSkipFirst:
1105
+ alphaOffset = 0
1106
+ default:
1107
+ return image
1108
+ }
1109
+
1110
+ var minX = width
1111
+ var minY = height
1112
+ var maxX = -1
1113
+ var maxY = -1
1114
+ let alphaThreshold: UInt8 = 8
1115
+
1116
+ for y in 0..<height {
1117
+ let rowStart = y * bytesPerRow
1118
+ for x in 0..<width {
1119
+ let pixelStart = rowStart + x * bytesPerPixel
1120
+ let alpha = bytes[pixelStart + alphaOffset]
1121
+ if alpha > alphaThreshold {
1122
+ minX = min(minX, x)
1123
+ minY = min(minY, y)
1124
+ maxX = max(maxX, x)
1125
+ maxY = max(maxY, y)
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
+ guard maxX >= minX, maxY >= minY else {
1131
+ return image
1132
+ }
1133
+
1134
+ let cropRect = CGRect(
1135
+ x: minX,
1136
+ y: minY,
1137
+ width: (maxX - minX + 1),
1138
+ height: (maxY - minY + 1)
1139
+ )
1140
+ guard let croppedCGImage = cgImage.cropping(to: cropRect) else {
1141
+ return image
1142
+ }
1143
+ return UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: .up)
1144
+ }
1145
+
1146
+ private func aspectFitRect(sourceSize: CGSize, targetSize: CGSize) -> CGRect {
1147
+ guard sourceSize.width > 0, sourceSize.height > 0, targetSize.width > 0, targetSize.height > 0 else {
1148
+ return CGRect(origin: .zero, size: targetSize)
1149
+ }
1150
+ let scale = min(targetSize.width / sourceSize.width, targetSize.height / sourceSize.height)
1151
+ let drawSize = CGSize(width: sourceSize.width * scale, height: sourceSize.height * scale)
1152
+ return CGRect(
1153
+ x: (targetSize.width - drawSize.width) / 2.0,
1154
+ y: (targetSize.height - drawSize.height) / 2.0,
1155
+ width: drawSize.width,
1156
+ height: drawSize.height
1157
+ )
1158
+ }
1159
+
1160
+ private func encodeTurnIconForLiveActivity(_ image: UIImage?) -> String? {
1161
+ guard let image else {
1162
+ return nil
1163
+ }
1164
+
1165
+ let maxEncodedLength = 2600
1166
+ let targetSizes: [CGSize] = [
1167
+ CGSize(width: 34, height: 34),
1168
+ CGSize(width: 30, height: 30),
1169
+ CGSize(width: 26, height: 26),
1170
+ CGSize(width: 28, height: 28),
1171
+ CGSize(width: 24, height: 24),
1172
+ CGSize(width: 20, height: 20)
1173
+ ]
1174
+ let preparedImage = trimmedTransparentBoundsImage(normalizedTurnIconImage(image))
1175
+
1176
+ for size in targetSizes {
1177
+ let renderer = UIGraphicsImageRenderer(size: size)
1178
+ let rendered = renderer.image { _ in
1179
+ let rect = aspectFitRect(sourceSize: preparedImage.size, targetSize: size)
1180
+ preparedImage.draw(in: rect)
1181
+ }
1182
+
1183
+ if let pngData = rendered.pngData() {
1184
+ let pngBase64 = pngData.base64EncodedString()
1185
+ if pngBase64.count <= maxEncodedLength {
1186
+ return pngBase64
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ NSLog("[ExpoGaodeMapNaviView][LiveActivity] turn icon dropped because encoded payload is too large")
1192
+ return nil
1193
+ }
1194
+
1195
+ private func fallbackTurnSymbolName(for iconType: Int) -> String {
1196
+ switch iconType {
1197
+ case 2:
1198
+ return "arrow.turn.up.left"
1199
+ case 3:
1200
+ return "arrow.turn.up.right"
1201
+ case 4:
1202
+ return "arrow.up.left"
1203
+ case 5:
1204
+ return "arrow.up.right"
1205
+ case 6:
1206
+ return "arrow.down.left"
1207
+ case 7:
1208
+ return "arrow.down.right"
1209
+ case 8:
1210
+ return "arrow.uturn.left"
1211
+ case 9, 20:
1212
+ return "arrow.up"
1213
+ case 11, 12:
1214
+ return "arrow.clockwise.circle"
1215
+ case 15:
1216
+ return "flag.checkered"
1217
+ case 19:
1218
+ return "arrow.uturn.right"
1219
+ case 65:
1220
+ return "arrow.left.circle"
1221
+ case 66:
1222
+ return "arrow.right.circle"
1223
+ default:
1224
+ return "arrow.up"
1225
+ }
1226
+ }
1227
+
1228
+ private func fallbackTurnIconImage(iconType: Int) -> UIImage? {
1229
+ let config = UIImage.SymbolConfiguration(pointSize: 36, weight: .bold)
1230
+ return UIImage(systemName: fallbackTurnSymbolName(for: iconType), withConfiguration: config)?
1231
+ .withTintColor(.white, renderingMode: .alwaysOriginal)
1232
+ }
1233
+
1234
+ private func resolveTravelTurnIconImage(iconType: Int) -> UIImage? {
1235
+ return fallbackTurnIconImage(iconType: iconType)
1236
+ }
1237
+
1238
+ private func updateCachedTurnIconForTravelSceneIfNeeded(iconType: Int) {
1239
+ guard activeScene != .drive else {
1240
+ return
1241
+ }
1242
+ let image = resolveTravelTurnIconImage(iconType: iconType)
1243
+ if let encoded = encodeTurnIconForLiveActivity(image) {
1244
+ lastTurnIconBase64 = encoded
1245
+ }
1246
+ lastTurnIconImageUri = cacheTurnIconImage(
1247
+ image,
1248
+ prefix: "travel_turn_icon",
1249
+ previousUri: lastTurnIconImageUri
1250
+ )
1251
+ }
1252
+
1253
+ private func cacheTurnIconImage(_ image: UIImage?, prefix: String, previousUri: String?) -> String? {
1254
+ guard let image, let data = image.pngData() else {
1255
+ if let previousUri {
1256
+ deleteCachedTurnIcon(at: previousUri)
1257
+ }
1258
+ return nil
1259
+ }
1260
+
1261
+ let filename = "\(prefix)_\(UUID().uuidString).png"
1262
+ let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
1263
+
1264
+ do {
1265
+ try data.write(to: fileURL, options: .atomic)
1266
+ if let previousUri, previousUri != fileURL.absoluteString {
1267
+ deleteCachedTurnIcon(at: previousUri)
1268
+ }
1269
+ return fileURL.absoluteString
1270
+ } catch {
1271
+ return previousUri
1272
+ }
1273
+ }
1274
+
1275
+ private func deleteCachedTurnIcon(at uriString: String) {
1276
+ guard let fileURL = URL(string: uriString), fileURL.isFileURL else {
1277
+ return
1278
+ }
1279
+ try? FileManager.default.removeItem(at: fileURL)
1280
+ }
1281
+
1282
+ private func clearCachedTurnIconUris() {
1283
+ if let lastTurnIconImageUri {
1284
+ deleteCachedTurnIcon(at: lastTurnIconImageUri)
1285
+ }
1286
+ if let lastNextTurnIconImageUri {
1287
+ deleteCachedTurnIcon(at: lastNextTurnIconImageUri)
1288
+ }
1289
+ lastTurnIconImageUri = nil
1290
+ lastNextTurnIconImageUri = nil
1291
+ lastTurnIconBase64 = nil
1292
+ }
1293
+
1294
+ private func splitLaneInfoString(_ value: String) -> [String] {
1295
+ value
1296
+ .split(separator: "|", omittingEmptySubsequences: false)
1297
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
1298
+ }
1299
+
1300
+ private func parseLaneToken(_ token: String, kind: LaneStringKind) -> Int? {
1301
+ guard !token.isEmpty else {
1302
+ return nil
1303
+ }
1304
+
1305
+ if token == "255" || token == "ff" {
1306
+ return 255
1307
+ }
1308
+
1309
+ // Older iOS callbacks may use `f` as filler / unavailable lane marker.
1310
+ if token == "f" {
1311
+ return kind == .background ? 255 : 255
1312
+ }
1313
+
1314
+ if let decimalValue = Int(token) {
1315
+ return decimalValue
1316
+ }
1317
+
1318
+ switch token {
1319
+ case "a":
1320
+ return 10
1321
+ case "b":
1322
+ return 11
1323
+ case "c":
1324
+ return 12
1325
+ case "d":
1326
+ return 13
1327
+ case "e":
1328
+ return 14
1329
+ case "g":
1330
+ return 16
1331
+ case "h":
1332
+ return 17
1333
+ case "i":
1334
+ return 18
1335
+ case "j":
1336
+ return 19
1337
+ case "k":
1338
+ return 20
1339
+ case "kk":
1340
+ return 21
1341
+ case "l":
1342
+ return 23
1343
+ default:
1344
+ return Int(token, radix: 16)
1345
+ }
1346
+ }
1347
+
1348
+ private func serializeLaneInfo(laneBackInfo: String, laneSelectInfo: String) -> [String: Any]? {
1349
+ let backgroundLane = splitLaneInfoString(laneBackInfo).map {
1350
+ parseLaneToken($0, kind: .background) ?? 255
1351
+ }
1352
+ let frontLane = splitLaneInfoString(laneSelectInfo).map {
1353
+ parseLaneToken($0, kind: .selected) ?? 255
1354
+ }
1355
+
1356
+ let sentinelIndex = backgroundLane.firstIndex(of: 255)
1357
+ let resolvedCount = [
1358
+ sentinelIndex,
1359
+ backgroundLane.isEmpty ? nil : backgroundLane.count,
1360
+ frontLane.isEmpty ? nil : frontLane.count
1361
+ ]
1362
+ .compactMap { $0 }
1363
+ .min() ?? 0
1364
+
1365
+ guard resolvedCount > 0 else {
1366
+ return nil
1367
+ }
1368
+
1369
+ let normalizedBackground = (0..<resolvedCount).map { index in
1370
+ backgroundLane.indices.contains(index) ? backgroundLane[index] : 255
1371
+ }
1372
+ let normalizedFront = (0..<resolvedCount).map { index in
1373
+ frontLane.indices.contains(index) ? frontLane[index] : 255
1374
+ }
1375
+
1376
+ return [
1377
+ "laneCount": resolvedCount,
1378
+ "backgroundLane": normalizedBackground,
1379
+ "frontLane": normalizedFront
1380
+ ]
1381
+ }
1382
+
1383
+ private func applyViewOptions() {
1384
+ switch activeScene {
1385
+ case .drive:
1386
+ applyDriveViewOptions()
1387
+ case .walk:
1388
+ applyWalkViewOptions()
1389
+ case .ride:
1390
+ applyRideViewOptions()
1391
+ }
1392
+ applyCustomAnnotationImages()
1393
+ applyCustomUILayoutOptionsIfNeeded()
1394
+ }
1395
+
1396
+ private func applyDriveViewOptions() {
1397
+ guard let driveView else {
1398
+ return
1399
+ }
1400
+ (driveView as? ExpoGaodeMapCustomDriveView)?.suppressBottomRightUI = showUIElements == false
1401
+ driveView.showUIElements = pendingShowUIElements ?? showUIElements
1402
+ driveView.showCamera = showCamera
1403
+ driveView.autoSwitchShowModeToCarPositionLocked = autoLockCar
1404
+ driveView.autoZoomMapLevel = autoChangeZoom
1405
+ driveView.mapShowTraffic = trafficLayerEnabled
1406
+ driveView.showCrossImage = realCrossDisplay
1407
+ driveView.showRoute = showRoute
1408
+ driveView.showTrafficBar = showTrafficBar
1409
+ driveView.showTrafficButton = showTrafficButton
1410
+ driveView.showBackupRoute = showBackupRoute
1411
+ driveView.showEagleMap = showEagleMap
1412
+ driveView.showVectorline = showVectorline
1413
+ driveView.showTrafficLights = showTrafficLights
1414
+ driveView.showDriveCongestion = showDriveCongestion
1415
+ driveView.showTrafficLightView = showTrafficLightView
1416
+ applyNaviMode(naviMode)
1417
+ applyShowMode(showMode)
1418
+ applyShowTurnArrow(showTurnArrow)
1419
+ applyShowBrowseRouteButton(showBrowseRouteButton)
1420
+ applyShowMoreButton(showMoreButton)
1421
+ applyShowGreyAfterPass(showGreyAfterPass)
1422
+ if let showCompassEnabled {
1423
+ applyShowCompassEnabled(showCompassEnabled)
1424
+ }
1425
+ applyLineWidth(lineWidth)
1426
+ applyScreenAnchor(screenAnchor)
1427
+ applyMapViewModeType(mapViewModeType)
1428
+ applyTrafficBarFrame(trafficBarFrame)
1429
+ applyTrafficBarColors(trafficBarColors)
1430
+ }
1431
+
1432
+ private func applyWalkViewOptions() {
1433
+ guard let walkView else {
1434
+ return
1435
+ }
1436
+ walkView.showUIElements = pendingShowUIElements ?? showUIElements
1437
+ applyNaviMode(naviMode)
1438
+ applyShowMode(showMode)
1439
+ applyShowTurnArrow(showTurnArrow)
1440
+ applyShowBrowseRouteButton(showBrowseRouteButton)
1441
+ applyShowMoreButton(showMoreButton)
1442
+ applyShowGreyAfterPass(showGreyAfterPass)
1443
+ if let showCompassEnabled {
1444
+ applyShowCompassEnabled(showCompassEnabled)
1445
+ }
1446
+ applyLineWidth(lineWidth)
1447
+ applyScreenAnchor(screenAnchor)
1448
+ applyMapViewModeType(mapViewModeType)
1449
+ }
1450
+
1451
+ private func applyRideViewOptions() {
1452
+ guard let rideView else {
1453
+ return
1454
+ }
1455
+ rideView.showUIElements = pendingShowUIElements ?? showUIElements
1456
+ applyNaviMode(naviMode)
1457
+ applyShowMode(showMode)
1458
+ applyShowTurnArrow(showTurnArrow)
1459
+ applyShowBrowseRouteButton(showBrowseRouteButton)
1460
+ applyShowMoreButton(showMoreButton)
1461
+ applyShowGreyAfterPass(showGreyAfterPass)
1462
+ if let showCompassEnabled {
1463
+ applyShowCompassEnabled(showCompassEnabled)
1464
+ }
1465
+ applyLineWidth(lineWidth)
1466
+ applyScreenAnchor(screenAnchor)
1467
+ applyMapViewModeType(mapViewModeType)
1468
+ }
1469
+
1470
+ private func applyCustomAnnotationImages() {
1471
+ applyCarImageSource()
1472
+ applyCarCompassImageSource()
1473
+ applyStartPointImageSource()
1474
+ applyWayPointImageSource()
1475
+ applyEndPointImageSource()
1476
+ applyCameraImageSource()
1477
+ }
1478
+
1479
+ private func applyCustomWaypointMarkerPayloads(_ payloads: [[String: Any]]?) {
1480
+ customWaypointMarkers = (payloads ?? []).compactMap { item in
1481
+ guard
1482
+ let latitude = item["latitude"] as? Double,
1483
+ let longitude = item["longitude"] as? Double
1484
+ else {
1485
+ return nil
1486
+ }
1487
+
1488
+ let title = (item["title"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
1489
+ return NaviCustomWaypointMarkerModel(
1490
+ latitude: latitude,
1491
+ longitude: longitude,
1492
+ title: (title?.isEmpty == false ? title : nil) ?? "途经"
1493
+ )
1494
+ }
1495
+
1496
+ refreshCustomWaypointAnnotations()
1497
+ }
1498
+
1499
+ private func clearCustomWaypointAnnotations() {
1500
+ guard let driveView else {
1501
+ renderedCustomWaypointAnnotations.removeAll()
1502
+ return
1503
+ }
1504
+
1505
+ for annotation in renderedCustomWaypointAnnotations {
1506
+ driveView.remove(annotation)
1507
+ }
1508
+ renderedCustomWaypointAnnotations.removeAll()
1509
+ }
1510
+
1511
+ private func refreshCustomWaypointAnnotations() {
1512
+ clearCustomWaypointAnnotations()
1513
+
1514
+ guard activeScene == .drive, let driveView else {
1515
+ return
1516
+ }
1517
+
1518
+ renderedCustomWaypointAnnotations = customWaypointMarkers.compactMap { marker in
1519
+ guard !marker.arrived else {
1520
+ return nil
1521
+ }
1522
+
1523
+ let coordinate = CLLocationCoordinate2D(
1524
+ latitude: marker.latitude,
1525
+ longitude: marker.longitude
1526
+ )
1527
+ let bubbleView = NaviCustomWaypointBubbleView(title: marker.title)
1528
+ guard let annotation = AMapNaviCompositeCustomAnnotation(
1529
+ coordinate: coordinate,
1530
+ view: bubbleView
1531
+ ) else {
1532
+ return nil
1533
+ }
1534
+
1535
+ driveView.add(annotation)
1536
+ return annotation
1537
+ }
1538
+ }
1539
+
1540
+ private func resetCustomWaypointArrivalState() {
1541
+ customWaypointMarkers = customWaypointMarkers.map { marker in
1542
+ var nextMarker = marker
1543
+ nextMarker.arrived = false
1544
+ return nextMarker
1545
+ }
1546
+ refreshCustomWaypointAnnotations()
1547
+ }
1548
+
1549
+ private func markNearestCustomWaypointArrived(_ point: AMapNaviPoint) {
1550
+ guard !customWaypointMarkers.isEmpty else {
1551
+ return
1552
+ }
1553
+
1554
+ let targetLatitude = Double(point.latitude)
1555
+ let targetLongitude = Double(point.longitude)
1556
+ let nextIndex = customWaypointMarkers.enumerated()
1557
+ .filter { !$0.element.arrived }
1558
+ .min { lhs, rhs in
1559
+ let lhsDistance = abs(lhs.element.latitude - targetLatitude) + abs(lhs.element.longitude - targetLongitude)
1560
+ let rhsDistance = abs(rhs.element.latitude - targetLatitude) + abs(rhs.element.longitude - targetLongitude)
1561
+ return lhsDistance < rhsDistance
1562
+ }?
1563
+ .offset
1564
+
1565
+ guard let nextIndex else {
1566
+ return
1567
+ }
1568
+
1569
+ customWaypointMarkers[nextIndex].arrived = true
1570
+ refreshCustomWaypointAnnotations()
1571
+ }
1572
+
1573
+ private func resolveLocalImage(_ source: String) -> UIImage? {
1574
+ if source.hasPrefix("file://") {
1575
+ let path = String(source.dropFirst(7))
1576
+ return UIImage(contentsOfFile: path)
1577
+ }
1578
+
1579
+ return UIImage(named: source) ?? UIImage(contentsOfFile: source)
1580
+ }
1581
+
1582
+ private func applyAnnotationImage(
1583
+ source: String?,
1584
+ currentSource: @escaping () -> String?,
1585
+ apply: @escaping (UIImage?) -> Void
1586
+ ) {
1587
+ guard let source, !source.isEmpty else {
1588
+ apply(nil)
1589
+ return
1590
+ }
1591
+
1592
+ if source.hasPrefix("http://") || source.hasPrefix("https://") {
1593
+ DispatchQueue.global().async {
1594
+ let image: UIImage? = {
1595
+ guard let url = URL(string: source),
1596
+ let data = try? Data(contentsOf: url) else {
1597
+ return nil
1598
+ }
1599
+ return UIImage(data: data)
1600
+ }()
1601
+
1602
+ DispatchQueue.main.async {
1603
+ guard currentSource() == source else {
1604
+ return
1605
+ }
1606
+ apply(image)
1607
+ }
1608
+ }
1609
+ return
1610
+ }
1611
+
1612
+ apply(resolveLocalImage(source))
1613
+ }
1614
+
1615
+ private func resizeImageIfNeeded(_ image: UIImage?, targetSize: CGSize?) -> UIImage? {
1616
+ guard let image else {
1617
+ return nil
1618
+ }
1619
+
1620
+ guard let targetSize, targetSize.width > 0, targetSize.height > 0 else {
1621
+ return image
1622
+ }
1623
+
1624
+ let renderer = UIGraphicsImageRenderer(size: targetSize)
1625
+ return renderer.image { _ in
1626
+ image.draw(in: CGRect(origin: .zero, size: targetSize))
1627
+ }
1628
+ }
1629
+
1630
+ private func applyCarImageSource() {
1631
+ applyAnnotationImage(source: carImageSource, currentSource: { [weak self] in
1632
+ self?.carImageSource
1633
+ }) { [weak self, weak driveView, weak walkView, weak rideView] image in
1634
+ let resizedImage = self?.resizeImageIfNeeded(image, targetSize: self?.carImageSize) ?? image
1635
+ driveView?.setCarImage(resizedImage)
1636
+ walkView?.setCarImage(resizedImage)
1637
+ rideView?.setCarImage(resizedImage)
1638
+ }
1639
+ }
1640
+
1641
+ private func applyCarCompassImageSource() {
1642
+ applyAnnotationImage(source: carCompassImageSource, currentSource: { [weak self] in
1643
+ self?.carCompassImageSource
1644
+ }) { [weak driveView, weak walkView, weak rideView] image in
1645
+ driveView?.setCarCompassImage(image)
1646
+ walkView?.setCarCompassImage(image)
1647
+ rideView?.setCarCompassImage(image)
1648
+ }
1649
+ }
1650
+
1651
+ private func applyStartPointImageSource() {
1652
+ applyAnnotationImage(source: startPointImageSource, currentSource: { [weak self] in
1653
+ self?.startPointImageSource
1654
+ }) { [weak driveView, weak walkView, weak rideView] image in
1655
+ driveView?.setStartPointImage(image)
1656
+ walkView?.setStartPointImage(image)
1657
+ rideView?.setStartPointImage(image)
1658
+ }
1659
+ }
1660
+
1661
+ private func applyWayPointImageSource() {
1662
+ guard let source = wayPointImageSource?.trimmingCharacters(in: .whitespacesAndNewlines), !source.isEmpty else {
1663
+ return
1664
+ }
1665
+ applyAnnotationImage(source: wayPointImageSource, currentSource: { [weak self] in
1666
+ self?.wayPointImageSource
1667
+ }) { [weak driveView, weak walkView, weak rideView] image in
1668
+ driveView?.setWayPointImage(image)
1669
+ walkView?.setWayPointImage(image)
1670
+ rideView?.setWayPointImage(image)
1671
+ }
1672
+ }
1673
+
1674
+ private func applyEndPointImageSource() {
1675
+ applyAnnotationImage(source: endPointImageSource, currentSource: { [weak self] in
1676
+ self?.endPointImageSource
1677
+ }) { [weak driveView, weak walkView, weak rideView] image in
1678
+ driveView?.setEndPointImage(image)
1679
+ walkView?.setEndPointImage(image)
1680
+ rideView?.setEndPointImage(image)
1681
+ }
1682
+ }
1683
+
1684
+ private func applyCameraImageSource() {
1685
+ guard let driveView else {
1686
+ return
1687
+ }
1688
+ applyAnnotationImage(source: cameraImageSource, currentSource: { [weak self] in
1689
+ self?.cameraImageSource
1690
+ }) { [weak driveView] image in
1691
+ driveView?.setCameraImage(image)
1692
+ }
1693
+ }
1694
+
1695
+ private func applyShowUIElementsToActiveViewIfReady() {
1696
+ guard currentNaviView() != nil else {
1697
+ pendingShowUIElements = showUIElements
1698
+ return
1699
+ }
1700
+ let value = pendingShowUIElements ?? showUIElements
1701
+ pendingShowUIElements = nil
1702
+
1703
+ switch activeScene {
1704
+ case .drive:
1705
+ (driveView as? ExpoGaodeMapCustomDriveView)?.suppressBottomRightUI = value == false
1706
+ driveView?.showUIElements = value
1707
+ case .walk:
1708
+ walkView?.showUIElements = value
1709
+ case .ride:
1710
+ rideView?.showUIElements = value
1711
+ }
1712
+
1713
+ applyCustomUILayoutOptionsIfNeeded()
1714
+ }
1715
+
1716
+ private func applyCustomUILayoutOptionsIfNeeded() {
1717
+ guard showUIElements == false else {
1718
+ (driveView as? ExpoGaodeMapCustomDriveView)?.suppressBottomRightUI = false
1719
+ return
1720
+ }
1721
+
1722
+ switch activeScene {
1723
+ case .drive:
1724
+ guard let driveView else {
1725
+ return
1726
+ }
1727
+ (driveView as? ExpoGaodeMapCustomDriveView)?.suppressBottomRightUI = true
1728
+ driveView.showCrossImage = realCrossDisplay
1729
+ driveView.showTrafficBar = showTrafficBar
1730
+ applyTrafficBarFrame(trafficBarFrame)
1731
+ applyTrafficBarColors(trafficBarColors)
1732
+ driveView.screenAnchor = screenAnchor
1733
+ driveView.setNeedsLayout()
1734
+ driveView.layoutIfNeeded()
1735
+ case .walk:
1736
+ guard let walkView else {
1737
+ return
1738
+ }
1739
+ walkView.screenAnchor = screenAnchor
1740
+ walkView.setNeedsLayout()
1741
+ walkView.layoutIfNeeded()
1742
+ case .ride:
1743
+ guard let rideView else {
1744
+ return
1745
+ }
1746
+ rideView.screenAnchor = screenAnchor
1747
+ rideView.setNeedsLayout()
1748
+ rideView.layoutIfNeeded()
1749
+ }
1750
+
1751
+ scheduleOverviewRouteVisibleRegionRefresh()
1752
+ }
1753
+
1754
+ private func refreshOverviewRouteVisibleRegionIfNeeded() {
1755
+ guard showUIElements == false else {
1756
+ return
1757
+ }
1758
+
1759
+ switch activeScene {
1760
+ case .drive:
1761
+ guard let driveView, driveView.showMode == .overview else {
1762
+ return
1763
+ }
1764
+ driveView.updateRoutePolylineInTheVisualRangeWhenTheShowModeIsOverview()
1765
+ case .walk:
1766
+ guard let walkView, walkView.showMode == .overview else {
1767
+ return
1768
+ }
1769
+ walkView.updateRoutePolylineInTheVisualRangeWhenTheShowModeIsOverview()
1770
+ case .ride:
1771
+ guard let rideView, rideView.showMode == .overview else {
1772
+ return
1773
+ }
1774
+ rideView.updateRoutePolylineInTheVisualRangeWhenTheShowModeIsOverview()
1775
+ }
1776
+ }
1777
+
1778
+ private func scheduleOverviewRouteVisibleRegionRefresh() {
1779
+ DispatchQueue.main.async { [weak self] in
1780
+ self?.currentNaviView()?.setNeedsLayout()
1781
+ self?.currentNaviView()?.layoutIfNeeded()
1782
+ self?.refreshOverviewRouteVisibleRegionIfNeeded()
1783
+ }
1784
+ }
1785
+
1786
+ private func applyHideNativeLaneInfoLayout(_ hidden: Bool) {
1787
+ (driveView as? ExpoGaodeMapCustomDriveView)?.suppressLaneInfoUI = hidden
1788
+ }
1789
+
1790
+ private func applyHideNativeTopInfoLayout(_ hidden: Bool) {
1791
+ (driveView as? ExpoGaodeMapCustomDriveView)?.suppressTopInfoUI = hidden
1792
+ driveView?.setNeedsLayout()
1793
+ driveView?.layoutIfNeeded()
1794
+ }
1795
+
1796
+ private func applyTrafficBarFrame(_ frame: CGRect) {
1797
+ guard let driveView else {
1798
+ return
1799
+ }
1800
+
1801
+ guard showTrafficBar, frame.width > 0, frame.height > 0 else {
1802
+ driveView.tmcRouteFrame = .zero
1803
+ return
1804
+ }
1805
+
1806
+ driveView.tmcRouteFrame = frame
1807
+ }
1808
+
1809
+ private func applyTrafficBarColors(_ colors: [String: Any]?) {
1810
+ guard let driveView, let colors else {
1811
+ return
1812
+ }
1813
+
1814
+ let mappings: [(String, AMapNaviRouteStatus)] = [
1815
+ ("unknown", .unknow),
1816
+ ("smooth", .smooth),
1817
+ ("fineOpen", .fineOpen),
1818
+ ("slow", .slow),
1819
+ ("jam", .jam),
1820
+ ("seriousJam", .seriousJam),
1821
+ ("defaultRoad", .default),
1822
+ ]
1823
+
1824
+ let resolvedColors = mappings.compactMap { key, status -> AMapNaviTMCStatusColor? in
1825
+ guard let value = colors[key], let color = ColorParser.parseColor(value) else {
1826
+ return nil
1827
+ }
1828
+ let item = AMapNaviTMCStatusColor()
1829
+ item.status = status
1830
+ item.color = color
1831
+ return item
1832
+ }
1833
+
1834
+ guard !resolvedColors.isEmpty else {
1835
+ return
1836
+ }
1837
+
1838
+ driveView.tmcRouteColor = resolvedColors
1839
+ }
1840
+
1841
+ private func emitTrafficStatusesUpdate(_ trafficStatuses: [AMapNaviTrafficStatus]?) {
1842
+ let totalLengthFromTraffic = (trafficStatuses ?? []).reduce(0) { partial, status in
1843
+ partial + max(status.length, 0)
1844
+ }
1845
+
1846
+ if totalLengthFromTraffic > 0 {
1847
+ if let retainDistance = lastNavigationInfoPayload?["pathRetainDistance"] as? Int {
1848
+ if totalLengthFromTraffic >= retainDistance {
1849
+ trafficBarTotalLength = totalLengthFromTraffic
1850
+ }
1851
+ } else {
1852
+ trafficBarTotalLength = totalLengthFromTraffic
1853
+ }
1854
+ }
1855
+
1856
+ // iOS 会提供 fineStatus;统一事件结构时保留它,方便 RN 自绘层按需细化颜色策略。
1857
+ let items = (trafficStatuses ?? []).map { status in
1858
+ var payload: [String: Any] = [
1859
+ "status": Int(status.status.rawValue),
1860
+ "length": status.length
1861
+ ]
1862
+ payload["fineStatus"] = status.trafficFineStatus
1863
+ return payload
1864
+ }
1865
+
1866
+ var payload: [String: Any] = [
1867
+ "items": items
1868
+ ]
1869
+
1870
+ let resolvedTotalLength = max(trafficBarTotalLength ?? 0, currentRouteTotalLength ?? 0)
1871
+ if resolvedTotalLength > 0 {
1872
+ payload["totalLength"] = resolvedTotalLength
1873
+ } else if totalLengthFromTraffic > 0 {
1874
+ payload["totalLength"] = totalLengthFromTraffic
1875
+ } else if let totalLength = currentRouteTotalLength, totalLength > 0 {
1876
+ payload["totalLength"] = totalLength
1877
+ }
1878
+
1879
+ if let retainDistance = lastNavigationInfoPayload?["pathRetainDistance"] as? Int {
1880
+ payload["retainDistance"] = retainDistance
1881
+ }
1882
+
1883
+ onTrafficStatusesUpdate(payload)
1884
+ }
1885
+
1886
+ // MARK: - Prop Setters
1887
+ private func applyShowCamera(_ show: Bool) {
1888
+ driveView?.showCamera = show
1889
+ }
1890
+
1891
+ private func applyEnableVoice(_ enabled: Bool) {
1892
+ driveManager?.isUseInternalTTS = enabled
1893
+ walkManager?.isUseInternalTTS = enabled
1894
+ rideManager?.isUseInternalTTS = enabled
1895
+ if enabled {
1896
+ activateNavigationAudioSessionIfNeeded(reason: "enable_voice_true")
1897
+ } else {
1898
+ deactivateNavigationAudioSessionIfNeeded(reason: "enable_voice_false")
1899
+ }
1900
+ // 内置语音会自动播报,不需要手动调用 startSpeak/stopSpeak
1901
+ }
1902
+
1903
+ private func applyAutoLockCar(_ enabled: Bool) {
1904
+ driveView?.autoSwitchShowModeToCarPositionLocked = enabled
1905
+ }
1906
+
1907
+ private func applyAutoChangeZoom(_ enabled: Bool) {
1908
+ driveView?.autoZoomMapLevel = enabled
1909
+ }
1910
+
1911
+ private func applyTrafficLayerEnabled(_ enabled: Bool) {
1912
+ driveView?.mapShowTraffic = enabled
1913
+ }
1914
+
1915
+ private func applyRealCrossDisplay(_ enabled: Bool) {
1916
+ driveView?.showCrossImage = enabled
1917
+ }
1918
+
1919
+ private func applyNaviMode(_ mode: Int) {
1920
+ let trackingMode: AMapNaviViewTrackingMode = mode == 0 ? .carNorth : .mapNorth
1921
+ switch activeScene {
1922
+ case .drive:
1923
+ driveView?.trackingMode = trackingMode
1924
+ case .walk:
1925
+ walkView?.trackingMode = trackingMode
1926
+ case .ride:
1927
+ rideView?.trackingMode = trackingMode
1928
+ }
1929
+ }
1930
+
1931
+ private func applyShowMode(_ mode: Int) {
1932
+ switch activeScene {
1933
+ case .drive:
1934
+ switch mode {
1935
+ case 1:
1936
+ driveView?.showMode = .carPositionLocked
1937
+ case 2:
1938
+ driveView?.showMode = .overview
1939
+ case 3:
1940
+ driveView?.showMode = .normal
1941
+ default:
1942
+ break
1943
+ }
1944
+ case .walk:
1945
+ switch mode {
1946
+ case 1:
1947
+ walkView?.showMode = .carPositionLocked
1948
+ case 2:
1949
+ walkView?.showMode = .overview
1950
+ case 3:
1951
+ walkView?.showMode = .normal
1952
+ default:
1953
+ break
1954
+ }
1955
+ case .ride:
1956
+ switch mode {
1957
+ case 1:
1958
+ rideView?.showMode = .carPositionLocked
1959
+ case 2:
1960
+ rideView?.showMode = .overview
1961
+ case 3:
1962
+ rideView?.showMode = .normal
1963
+ default:
1964
+ break
1965
+ }
1966
+ }
1967
+ scheduleOverviewRouteVisibleRegionRefresh()
1968
+ }
1969
+
1970
+ private func applyNightMode(_ enabled: Bool) {
1971
+ applyMapViewModeType(enabled ? 1 : 0)
1972
+ }
1973
+
1974
+ private func applyMapViewModeType(_ type: Int) {
1975
+ let resolvedMode: AMapNaviViewMapModeType
1976
+ switch type {
1977
+ case 0:
1978
+ resolvedMode = .day
1979
+ case 1:
1980
+ resolvedMode = .night
1981
+ case 2:
1982
+ resolvedMode = .dayNightAuto
1983
+ case 3:
1984
+ resolvedMode = .custom
1985
+ default:
1986
+ return
1987
+ }
1988
+
1989
+ switch activeScene {
1990
+ case .drive:
1991
+ driveView?.mapViewModeType = resolvedMode
1992
+ case .walk:
1993
+ walkView?.mapViewModeType = resolvedMode
1994
+ case .ride:
1995
+ rideView?.mapViewModeType = resolvedMode
1996
+ }
1997
+ }
1998
+
1999
+ private func applyShowTurnArrow(_ visible: Bool) {
2000
+ switch activeScene {
2001
+ case .drive:
2002
+ driveView?.showTurnArrow = visible
2003
+ case .walk:
2004
+ walkView?.showTurnArrow = visible
2005
+ case .ride:
2006
+ rideView?.showTurnArrow = visible
2007
+ }
2008
+ }
2009
+
2010
+ private func applyShowBrowseRouteButton(_ visible: Bool) {
2011
+ switch activeScene {
2012
+ case .drive:
2013
+ driveView?.showBrowseRouteButton = visible
2014
+ case .walk:
2015
+ walkView?.showBrowseRouteButton = visible
2016
+ case .ride:
2017
+ rideView?.showBrowseRouteButton = visible
2018
+ }
2019
+ }
2020
+
2021
+ private func applyShowMoreButton(_ visible: Bool) {
2022
+ switch activeScene {
2023
+ case .drive:
2024
+ driveView?.showMoreButton = visible
2025
+ case .walk:
2026
+ walkView?.showMoreButton = visible
2027
+ case .ride:
2028
+ rideView?.showMoreButton = visible
2029
+ }
2030
+ }
2031
+
2032
+ private func applyShowGreyAfterPass(_ enabled: Bool) {
2033
+ switch activeScene {
2034
+ case .drive:
2035
+ driveView?.showGreyAfterPass = enabled
2036
+ case .walk:
2037
+ walkView?.showGreyAfterPass = enabled
2038
+ case .ride:
2039
+ rideView?.showGreyAfterPass = enabled
2040
+ }
2041
+ }
2042
+
2043
+ private func applyShowCompassEnabled(_ enabled: Bool) {
2044
+ switch activeScene {
2045
+ case .drive:
2046
+ driveView?.showCompass = enabled
2047
+ case .walk:
2048
+ walkView?.showCompass = enabled
2049
+ case .ride:
2050
+ rideView?.showCompass = enabled
2051
+ }
2052
+ }
2053
+
2054
+ private func applyLineWidth(_ width: CGFloat) {
2055
+ switch activeScene {
2056
+ case .drive:
2057
+ driveView?.lineWidth = width
2058
+ case .walk:
2059
+ walkView?.lineWidth = width
2060
+ case .ride:
2061
+ rideView?.lineWidth = width
2062
+ }
2063
+ }
2064
+
2065
+ private func applyScreenAnchor(_ anchor: CGPoint) {
2066
+ switch activeScene {
2067
+ case .drive:
2068
+ driveView?.screenAnchor = anchor
2069
+ case .walk:
2070
+ walkView?.screenAnchor = anchor
2071
+ case .ride:
2072
+ rideView?.screenAnchor = anchor
2073
+ }
2074
+ }
2075
+
2076
+ private func applyBackgroundLocationOptionsForActiveScene() {
2077
+ switch activeScene {
2078
+ case .drive:
2079
+ applyDriveManagerBackgroundLocationOptionsIfNeeded()
2080
+ case .walk:
2081
+ applyWalkManagerBackgroundLocationOptionsIfNeeded()
2082
+ case .ride:
2083
+ applyRideManagerBackgroundLocationOptionsIfNeeded()
2084
+ }
2085
+ }
2086
+
2087
+ private func handleNavigationStarted(_ naviMode: AMapNaviMode) {
2088
+ hasStartedNavi = true
2089
+ applyBackgroundLocationOptionsForActiveScene()
2090
+ activateNavigationAudioSessionIfNeeded(reason: "did_start_navi")
2091
+ onNavigationStarted([
2092
+ "type": naviMode == .emulator ? 1 : 0,
2093
+ "isEmulator": naviMode == .emulator
2094
+ ])
2095
+ applyShowUIElementsToActiveViewIfReady()
2096
+ refreshCustomWaypointAnnotations()
2097
+ syncNavigationLiveActivityWithLastPayload()
2098
+ }
2099
+
2100
+ private func handleNavigationLocationUpdate(_ naviLocation: AMapNaviLocation) {
2101
+ if !hasReceivedFirstNaviData {
2102
+ hasReceivedFirstNaviData = true
2103
+ applyShowUIElementsToActiveViewIfReady()
2104
+ }
2105
+ lastKnownSpeed = Int(naviLocation.speed)
2106
+ onLocationUpdate([
2107
+ "latitude": naviLocation.coordinate.latitude,
2108
+ "longitude": naviLocation.coordinate.longitude,
2109
+ "speed": naviLocation.speed,
2110
+ "bearing": naviLocation.heading
2111
+ ])
2112
+ }
2113
+
2114
+ private func navigationInfoPayload(from naviInfo: AMapNaviInfo) -> [String: Any] {
2115
+ var payload: [String: Any] = [
2116
+ "naviMode": naviInfo.naviMode.rawValue,
2117
+ "currentRoadName": naviInfo.currentRoadName ?? "",
2118
+ "nextRoadName": naviInfo.nextRoadName ?? "",
2119
+ "pathRetainDistance": naviInfo.routeRemainDistance,
2120
+ "pathRetainTime": naviInfo.routeRemainTime,
2121
+ "curStepRetainDistance": naviInfo.segmentRemainDistance,
2122
+ "curStepRetainTime": naviInfo.segmentRemainTime,
2123
+ "currentSpeed": lastKnownSpeed,
2124
+ "iconType": naviInfo.iconType.rawValue,
2125
+ "iconDirection": naviInfo.iconType.rawValue,
2126
+ "currentSegmentIndex": naviInfo.currentSegmentIndex,
2127
+ "currentLinkIndex": naviInfo.currentLinkIndex,
2128
+ "currentPointIndex": naviInfo.currentPointIndex,
2129
+ "routeRemainTrafficLightCount": naviInfo.routeRemainTrafficLightCount,
2130
+ "driveDistance": naviInfo.routeDriveDistance,
2131
+ "driveTime": naviInfo.routeDriveTime
2132
+ ]
2133
+
2134
+ if let nextIconType = resolveNextTurnIconType(currentSegmentIndex: naviInfo.currentSegmentIndex) {
2135
+ payload["nextIconType"] = nextIconType
2136
+ }
2137
+
2138
+ return payload
2139
+ }
2140
+
2141
+ private func handleNavigationInfoUpdate(_ naviInfo: AMapNaviInfo) {
2142
+ if !hasReceivedFirstNaviData {
2143
+ hasReceivedFirstNaviData = true
2144
+ applyShowUIElementsToActiveViewIfReady()
2145
+ }
2146
+ updateCachedTurnIconForTravelSceneIfNeeded(iconType: Int(naviInfo.iconType.rawValue))
2147
+ emitNavigationInfoUpdate(navigationInfoPayload(from: naviInfo))
2148
+ }
2149
+
2150
+ private func resolveNextTurnIconType(currentSegmentIndex: Int) -> Int? {
2151
+ guard currentSegmentIndex >= 0 else {
2152
+ return nil
2153
+ }
2154
+
2155
+ guard let routeSegments = currentNaviRoute?.routeSegments else {
2156
+ return nil
2157
+ }
2158
+
2159
+ let nextSegmentIndex = currentSegmentIndex + 1
2160
+ guard routeSegments.indices.contains(nextSegmentIndex) else {
2161
+ return nil
2162
+ }
2163
+
2164
+ let nextIconType = Int(routeSegments[nextSegmentIndex].iconType.rawValue)
2165
+ return nextIconType > 0 ? nextIconType : nil
2166
+ }
2167
+
2168
+ private func handleNavigationSound(_ soundString: String, soundStringType: AMapNaviSoundType) {
2169
+ activateNavigationAudioSessionIfNeeded(reason: "play_navi_sound")
2170
+ onNavigationText([
2171
+ "type": soundStringType.rawValue,
2172
+ "text": soundString
2173
+ ])
2174
+ }
2175
+
2176
+ private func handleGpsSignalUpdate(_ gpsSignalStrength: AMapNaviGPSSignalStrength) {
2177
+ let rawValue = gpsSignalStrength.rawValue
2178
+ let isWeak = rawValue == AMapNaviGPSSignalStrength.weak.rawValue || rawValue == 0
2179
+ onGpsSignalWeak([
2180
+ "isWeak": isWeak
2181
+ ])
2182
+ }
2183
+
2184
+ private func handleWayPointArrived(index: Int, point: AMapNaviPoint? = nil) {
2185
+ if let point {
2186
+ markNearestCustomWaypointArrived(point)
2187
+ }
2188
+ onWayPointArrived([
2189
+ "index": index
2190
+ ])
2191
+ }
2192
+
2193
+ private func handleDidEndEmulatorNavi(reason: String) {
2194
+ resetTransientNavigationState()
2195
+ deactivateNavigationAudioSessionIfNeeded(reason: reason)
2196
+ onNavigationEnded([:])
2197
+ }
2198
+
2199
+ // MARK: - Public Methods
2200
+ func startNavigation(startLat: Double, startLng: Double, endLat: Double, endLng: Double, promise: Promise) {
404
2201
  // 再次检查隐私状态和初始化状态
405
2202
  do {
406
2203
  try checkPrivacyReady()
@@ -419,7 +2216,10 @@ public class ExpoGaodeMapNaviView: ExpoView {
419
2216
  promise.reject(code, formatError(error))
420
2217
  return
421
2218
  }
422
-
2219
+
2220
+ switchToScene(.drive)
2221
+ resetTransientNavigationState()
2222
+
423
2223
  startCoordinate = AMapNaviPoint.location(withLatitude: CGFloat(startLat), longitude: CGFloat(startLng))
424
2224
  endCoordinate = AMapNaviPoint.location(withLatitude: CGFloat(endLat), longitude: CGFloat(endLng))
425
2225
 
@@ -446,9 +2246,69 @@ public class ExpoGaodeMapNaviView: ExpoView {
446
2246
  promise.reject("CALCULATE_FAILED", "启动路线规划失败")
447
2247
  }
448
2248
  }
2249
+
2250
+ func startNavigationWithIndependentPath(
2251
+ token: Int,
2252
+ routeId: Int?,
2253
+ routeIndex: Int?,
2254
+ naviType: Int?,
2255
+ promise: Promise
2256
+ ) {
2257
+ do {
2258
+ try checkPrivacyReady()
2259
+ try checkAMapInitialization()
2260
+ try ensureBackgroundLocationModeForNavigation()
2261
+ } catch {
2262
+ let nsError = error as NSError
2263
+ let code: String
2264
+ if nsError.domain == "ExpoGaodeMapPrivacy" {
2265
+ code = "PRIVACY_NOT_AGREED"
2266
+ } else if nsError.code == -1003 {
2267
+ code = "BACKGROUND_LOCATION_NOT_ENABLED"
2268
+ } else {
2269
+ code = "AMAP_NOT_INITIALIZED"
2270
+ }
2271
+ promise.reject(code, formatError(error))
2272
+ return
2273
+ }
2274
+
2275
+ let scene = NaviScene(rawValue: independentRouteManager.scene(for: token) ?? "") ?? .drive
2276
+ switchToScene(scene)
2277
+ resetTransientNavigationState()
2278
+
2279
+ if let naviType {
2280
+ self.naviType = naviType
2281
+ }
2282
+
2283
+ let ok = independentRouteManager.start(
2284
+ token: token,
2285
+ naviType: self.naviType,
2286
+ routeId: routeId,
2287
+ routeIndex: routeIndex
2288
+ )
2289
+
2290
+ if ok {
2291
+ promise.resolve([
2292
+ "success": true,
2293
+ "message": "独立路径导航启动中...",
2294
+ "token": token,
2295
+ "naviType": self.naviType
2296
+ ])
2297
+ } else {
2298
+ promise.reject("START_INDEPENDENT_NAVI_FAILED", "独立路径导航启动失败")
2299
+ }
2300
+ }
449
2301
 
450
2302
  func stopNavigation(promise: Promise) {
451
- driveManager?.stopNavi()
2303
+ switch activeScene {
2304
+ case .drive:
2305
+ driveManager?.stopNavi()
2306
+ case .walk:
2307
+ walkManager?.stopNavi()
2308
+ case .ride:
2309
+ rideManager?.stopNavi()
2310
+ }
2311
+ resetTransientNavigationState()
452
2312
  promise.resolve([
453
2313
  "success": true,
454
2314
  "message": "导航已停止"
@@ -456,7 +2316,21 @@ public class ExpoGaodeMapNaviView: ExpoView {
456
2316
  }
457
2317
 
458
2318
  func playCustomTTS(text: String, forcePlay: Bool, promise: Promise) {
459
- // iOS 使用内置 TTS,会自动播报导航语音
2319
+ guard currentNaviManager() != nil else {
2320
+ promise.reject("NAVI_MANAGER_UNAVAILABLE", "导航管理器尚未初始化")
2321
+ return
2322
+ }
2323
+
2324
+ if forcePlay {
2325
+ customSpeechSynthesizer.stopSpeaking(at: .immediate)
2326
+ } else if customSpeechSynthesizer.isSpeaking {
2327
+ promise.reject("PLAY_TTS_FAILED", "当前正在播报其他导航语音")
2328
+ return
2329
+ }
2330
+
2331
+ let utterance = AVSpeechUtterance(string: text)
2332
+ utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
2333
+ customSpeechSynthesizer.speak(utterance)
460
2334
  promise.resolve([
461
2335
  "success": true
462
2336
  ])
@@ -466,22 +2340,63 @@ public class ExpoGaodeMapNaviView: ExpoView {
466
2340
  public override func layoutSubviews() {
467
2341
  super.layoutSubviews()
468
2342
  driveView?.frame = bounds
2343
+ walkView?.frame = bounds
2344
+ rideView?.frame = bounds
2345
+ scheduleOverviewRouteVisibleRegionRefresh()
469
2346
  }
470
2347
 
471
2348
  deinit {
2349
+ NavigationLiveActivityManager.shared.stop()
2350
+ deactivateNavigationAudioSessionIfNeeded(reason: "deinit")
472
2351
  driveManager?.stopNavi()
473
- if let view = driveView {
474
- driveManager?.removeDataRepresentative(view)
475
- }
476
- driveManager?.delegate = nil
2352
+ walkManager?.stopNavi()
2353
+ rideManager?.stopNavi()
2354
+ clearCustomWaypointAnnotations()
2355
+ destroyDriveManagerIfNeeded()
2356
+ destroyWalkManagerIfNeeded()
2357
+ destroyRideManagerIfNeeded()
2358
+ clearCachedTurnIconUris()
477
2359
  }
478
2360
  }
479
2361
 
480
2362
  // MARK: - AMapNaviDriveManagerDelegate
481
2363
  extension ExpoGaodeMapNaviView: AMapNaviDriveManagerDelegate {
2364
+ private func makeArrivedLiveActivitySnapshot() -> NavigationLiveActivitySnapshot {
2365
+ let remainDistance = payloadIntValue(lastNavigationInfoPayload?["pathRetainDistance"]) ?? 0
2366
+ let routeTotalDistance = max(
2367
+ trafficBarTotalLength ?? 0,
2368
+ currentRouteTotalLength ?? 0,
2369
+ remainDistance
2370
+ )
2371
+ return NavigationLiveActivitySnapshot(
2372
+ appName: resolveAppDisplayName(),
2373
+ currentRoadName: "",
2374
+ nextRoadName: "",
2375
+ pathRetainDistance: 0,
2376
+ routeTotalDistance: routeTotalDistance,
2377
+ pathRetainTime: 0,
2378
+ curStepRetainDistance: 0,
2379
+ iconType: 15,
2380
+ turnIconBase64: nil
2381
+ )
2382
+ }
2383
+
2384
+ private func handleArrivedDestination(source: String) {
2385
+ NSLog("[ExpoGaodeMapNaviView][LiveActivity] arrived destination callback received: %@", source)
2386
+ hasStartedNavi = false
2387
+ if iosLiveActivityEnabled {
2388
+ let arrivedSnapshot = makeArrivedLiveActivitySnapshot()
2389
+ NavigationLiveActivityManager.shared.showArrivedAndStop(snapshot: arrivedSnapshot, dismissAfter: 6)
2390
+ } else {
2391
+ NavigationLiveActivityManager.shared.stop()
2392
+ }
2393
+ onArriveDestination([:])
2394
+ }
482
2395
 
483
2396
  public func driveManager(onCalculateRouteSuccess driveManager: AMapNaviDriveManager) {
484
2397
  hasStartedNavi = true
2398
+ applyDriveManagerBackgroundLocationOptionsIfNeeded()
2399
+ activateNavigationAudioSessionIfNeeded(reason: "calculate_route_success")
485
2400
  onRouteCalculated([
486
2401
  "success": true,
487
2402
  "naviType": naviType
@@ -494,7 +2409,11 @@ extension ExpoGaodeMapNaviView: AMapNaviDriveManagerDelegate {
494
2409
  driveManager.startGPSNavi()
495
2410
  }
496
2411
 
497
- applyShowUIElementsToDriveViewIfReady()
2412
+ applyShowUIElementsToActiveViewIfReady()
2413
+ DispatchQueue.main.async { [weak self] in
2414
+ self?.applyCustomAnnotationImages()
2415
+ self?.refreshCustomWaypointAnnotations()
2416
+ }
498
2417
  }
499
2418
 
500
2419
  public func driveManager(_ driveManager: AMapNaviDriveManager, onCalculateRouteFailure error: Error) {
@@ -505,81 +2424,47 @@ extension ExpoGaodeMapNaviView: AMapNaviDriveManagerDelegate {
505
2424
  }
506
2425
 
507
2426
  public func driveManager(_ driveManager: AMapNaviDriveManager, didStartNavi naviMode: AMapNaviMode) {
508
- hasStartedNavi = true
509
- onNavigationStarted([
510
- "type": naviMode == .emulator ? 1 : 0,
511
- "isEmulator": naviMode == .emulator
512
- ])
513
-
514
- applyShowUIElementsToDriveViewIfReady()
2427
+ handleNavigationStarted(naviMode)
515
2428
  }
516
2429
 
517
2430
  public func driveManagerNavi(_ driveManager: AMapNaviDriveManager, didArrive wayPoint: AMapNaviPoint) {
518
- onWayPointArrived([
519
- "index": 0
520
- ])
2431
+ handleWayPointArrived(index: 0, point: wayPoint)
521
2432
  }
522
2433
 
523
2434
  public func driveManagerDidEndEmulatorNavi(_ driveManager: AMapNaviDriveManager) {
524
- onNavigationEnded([:])
525
- }
526
-
527
- public func driveManager(_ driveManager: AMapNaviDriveManager, didUpdate naviLocation: AMapNaviLocation) {
528
- if !hasReceivedFirstNaviData {
529
- hasReceivedFirstNaviData = true
530
- applyShowUIElementsToDriveViewIfReady()
531
- }
532
- onLocationUpdate([
533
- "latitude": naviLocation.coordinate.latitude,
534
- "longitude": naviLocation.coordinate.longitude,
535
- "speed": naviLocation.speed,
536
- "bearing": naviLocation.heading
537
- ])
538
- }
539
-
540
- public func driveManager(_ driveManager: AMapNaviDriveManager, didUpdate naviInfo: AMapNaviInfo) {
541
- if !hasReceivedFirstNaviData {
542
- hasReceivedFirstNaviData = true
543
- applyShowUIElementsToDriveViewIfReady()
544
- }
545
- onNavigationInfoUpdate([
546
- "currentRoadName": naviInfo.currentRoadName ?? "",
547
- "nextRoadName": naviInfo.nextRoadName ?? "",
548
- "pathRetainDistance": naviInfo.routeRemainDistance,
549
- "pathRetainTime": naviInfo.routeRemainTime,
550
- "curStepRetainDistance": naviInfo.segmentRemainDistance,
551
- "curStepRetainTime": naviInfo.segmentRemainTime
552
- ])
2435
+ handleDidEndEmulatorNavi(reason: "did_end_emulator_navi")
553
2436
  }
554
2437
 
555
2438
  public func driveManager(_ driveManager: AMapNaviDriveManager, playNaviSound soundString: String, soundStringType: AMapNaviSoundType) {
556
- onNavigationText([
557
- "type": soundStringType.rawValue,
558
- "text": soundString
559
- ])
2439
+ handleNavigationSound(soundString, soundStringType: soundStringType)
560
2440
  }
561
2441
 
2442
+ /// 兼容一部分 SDK/Swift 导入下的到达终点回调签名
562
2443
  public func driveManager(onArrivedDestination driveManager: AMapNaviDriveManager) {
563
- onArriveDestination([:])
2444
+ handleArrivedDestination(source: "driveManager(onArrivedDestination:)")
2445
+ deactivateNavigationAudioSessionIfNeeded(reason: "arrived_destination_named")
2446
+ }
2447
+
2448
+ /// AMapNaviDriveManagerDelegate 官方到达终点回调
2449
+ public func driveManagerOnArrivedDestination(_ driveManager: AMapNaviDriveManager) {
2450
+ handleArrivedDestination(source: "driveManagerOnArrivedDestination")
2451
+ deactivateNavigationAudioSessionIfNeeded(reason: "arrived_destination_official")
564
2452
  }
565
2453
 
566
- public func driveManagerOnReCalculateRoute(forYaw driveManager: AMapNaviDriveManager) {
2454
+ public func driveManagerNeedRecalculateRoute(forYaw driveManager: AMapNaviDriveManager) {
567
2455
  onRouteRecalculate([
568
2456
  "reason": "yaw"
569
2457
  ])
570
2458
  }
571
2459
 
572
- public func driveManagerOnReCalculateRoute(forTrafficJam driveManager: AMapNaviDriveManager) {
2460
+ public func driveManagerNeedRecalculateRoute(forTrafficJam driveManager: AMapNaviDriveManager) {
573
2461
  onRouteRecalculate([
574
2462
  "reason": "traffic"
575
2463
  ])
576
2464
  }
577
2465
 
578
2466
  public func driveManager(_ driveManager: AMapNaviDriveManager, update gpsSignalStrength: AMapNaviGPSSignalStrength) {
579
- let isWeak = gpsSignalStrength == .weak || gpsSignalStrength == .none
580
- onGpsSignalWeak([
581
- "isWeak": isWeak
582
- ])
2467
+ handleGpsSignalUpdate(gpsSignalStrength)
583
2468
  }
584
2469
  }
585
2470
 
@@ -587,13 +2472,258 @@ extension ExpoGaodeMapNaviView: AMapNaviDriveManagerDelegate {
587
2472
  extension ExpoGaodeMapNaviView: AMapNaviDriveViewDelegate {
588
2473
 
589
2474
  public func driveViewDidLoad(_ driveView: AMapNaviDriveView) {
590
- driveViewLoaded = true
591
2475
  applyViewOptions()
592
- applyShowUIElementsToDriveViewIfReady()
2476
+ applyShowUIElementsToActiveViewIfReady()
2477
+ refreshCustomWaypointAnnotations()
2478
+ scheduleOverviewRouteVisibleRegionRefresh()
593
2479
  onNavigationReady([:])
594
2480
  }
595
2481
 
596
2482
  public func driveViewEdgePadding(_ driveView: AMapNaviDriveView) -> UIEdgeInsets {
597
- return .zero
2483
+ return driveViewEdgePadding
2484
+ }
2485
+
2486
+ public func driveView(_ driveView: AMapNaviDriveView, didChange showMode: AMapNaviDriveViewShowMode) {
2487
+ if showMode == .overview {
2488
+ scheduleOverviewRouteVisibleRegionRefresh()
2489
+ }
2490
+ }
2491
+ }
2492
+
2493
+ extension ExpoGaodeMapNaviView: AMapNaviDriveDataRepresentable {
2494
+ public func driveManager(_ driveManager: AMapNaviDriveManager, update naviRoute: AMapNaviRoute?) {
2495
+ currentNaviRoute = naviRoute
2496
+ if let routeLength = naviRoute?.routeLength, routeLength > 0 {
2497
+ currentRouteTotalLength = routeLength
2498
+ }
2499
+ }
2500
+
2501
+ public func driveManager(_ driveManager: AMapNaviDriveManager, update naviLocation: AMapNaviLocation?) {
2502
+ guard let naviLocation else {
2503
+ return
2504
+ }
2505
+ handleNavigationLocationUpdate(naviLocation)
2506
+ }
2507
+
2508
+ public func driveManager(_ driveManager: AMapNaviDriveManager, update naviInfo: AMapNaviInfo?) {
2509
+ guard let naviInfo else {
2510
+ return
2511
+ }
2512
+ handleNavigationInfoUpdate(naviInfo)
2513
+ }
2514
+
2515
+ public func driveManager(_ driveManager: AMapNaviDriveManager, showCross crossImage: UIImage?) {
2516
+ isCrossVisible = true
2517
+ emitVisualStateUpdate()
2518
+ }
2519
+
2520
+ public func driveManagerHideCrossImage(_ driveManager: AMapNaviDriveManager) {
2521
+ isCrossVisible = false
2522
+ emitVisualStateUpdate()
2523
+ }
2524
+
2525
+ public func driveManager(_ driveManager: AMapNaviDriveManager, showLaneBackInfo laneBackInfo: String, laneSelectInfo: String) {
2526
+ isLaneInfoVisible = true
2527
+ emitVisualStateUpdate()
2528
+ if let payload = serializeLaneInfo(laneBackInfo: laneBackInfo, laneSelectInfo: laneSelectInfo) {
2529
+ onLaneInfoUpdate(payload)
2530
+ }
2531
+ }
2532
+
2533
+ public func driveManagerHideLaneInfo(_ driveManager: AMapNaviDriveManager) {
2534
+ isLaneInfoVisible = false
2535
+ emitVisualStateUpdate()
2536
+ }
2537
+
2538
+ public func driveManager(_ driveManager: AMapNaviDriveManager, updateTrafficStatus trafficStatus: [AMapNaviTrafficStatus]?) {
2539
+ emitTrafficStatusesUpdate(trafficStatus)
2540
+ }
2541
+
2542
+ public func driveManager(_ driveManager: AMapNaviDriveManager, updateTurnIconImage turnIconImage: UIImage?, turn turnIconType: AMapNaviIconType) {
2543
+ lastTurnIconType = Int(turnIconType.rawValue)
2544
+ let encodedTurnIcon = encodeTurnIconForLiveActivity(turnIconImage)
2545
+ if let encodedTurnIcon {
2546
+ lastTurnIconBase64 = encodedTurnIcon
2547
+ } else if turnIconImage == nil {
2548
+ NSLog(
2549
+ "[ExpoGaodeMapNaviView][LiveActivity] turnIconImage is nil, keep previous turn icon snapshot"
2550
+ )
2551
+ }
2552
+ NSLog(
2553
+ "[ExpoGaodeMapNaviView][LiveActivity] turnIconType=%d, incomingBase64Length=%d, effectiveBase64Length=%d",
2554
+ Int(turnIconType.rawValue),
2555
+ encodedTurnIcon?.count ?? 0,
2556
+ lastTurnIconBase64?.count ?? 0
2557
+ )
2558
+ lastTurnIconImageUri = cacheTurnIconImage(
2559
+ turnIconImage,
2560
+ prefix: "turn_icon",
2561
+ previousUri: lastTurnIconImageUri
2562
+ )
2563
+ reemitLastNavigationInfoIfNeeded()
2564
+ }
2565
+
2566
+ public func driveManager(_ driveManager: AMapNaviDriveManager, updateNextTurnIconImage turnIconImage: UIImage?, nextTurn turnIconType: AMapNaviIconType) {
2567
+ lastNextTurnIconType = Int(turnIconType.rawValue)
2568
+ lastNextTurnIconImageUri = cacheTurnIconImage(
2569
+ turnIconImage,
2570
+ prefix: "next_turn_icon",
2571
+ previousUri: lastNextTurnIconImageUri
2572
+ )
2573
+ reemitLastNavigationInfoIfNeeded()
2574
+ }
2575
+ }
2576
+
2577
+ // MARK: - AMapNaviWalkManagerDelegate
2578
+ extension ExpoGaodeMapNaviView: AMapNaviWalkManagerDelegate {
2579
+ public func walkManager(_ walkManager: AMapNaviWalkManager, didStartNavi naviMode: AMapNaviMode) {
2580
+ handleNavigationStarted(naviMode)
2581
+ }
2582
+
2583
+ public func walkManagerDidEndEmulatorNavi(_ walkManager: AMapNaviWalkManager) {
2584
+ handleDidEndEmulatorNavi(reason: "walk_did_end_emulator_navi")
2585
+ }
2586
+
2587
+ public func walkManager(_ walkManager: AMapNaviWalkManager, playNaviSound soundString: String, soundStringType: AMapNaviSoundType) {
2588
+ handleNavigationSound(soundString, soundStringType: soundStringType)
2589
+ }
2590
+
2591
+ public func walkManager(onArrivedDestination walkManager: AMapNaviWalkManager) {
2592
+ handleArrivedDestination(source: "walkManagerOnArrivedDestination")
2593
+ deactivateNavigationAudioSessionIfNeeded(reason: "walk_arrived_destination")
2594
+ }
2595
+
2596
+ public func walkManagerNeedRecalculateRoute(forYaw walkManager: AMapNaviWalkManager) {
2597
+ onRouteRecalculate([
2598
+ "reason": "yaw"
2599
+ ])
2600
+ }
2601
+
2602
+ public func walkManager(_ walkManager: AMapNaviWalkManager, update gpsSignalStrength: AMapNaviGPSSignalStrength) {
2603
+ handleGpsSignalUpdate(gpsSignalStrength)
2604
+ }
2605
+
2606
+ public func walkManager(_ walkManager: AMapNaviWalkManager, onArrivedWayPoint wayPointIndex: Int32) {
2607
+ handleWayPointArrived(index: Int(wayPointIndex))
2608
+ }
2609
+
2610
+ public func walkManager(_ walkManager: AMapNaviWalkManager, onCalculateRouteFailure error: Error) {
2611
+ onRouteCalculated([
2612
+ "success": false,
2613
+ "errorInfo": error.localizedDescription
2614
+ ])
2615
+ }
2616
+ }
2617
+
2618
+ // MARK: - AMapNaviWalkViewDelegate
2619
+ extension ExpoGaodeMapNaviView: AMapNaviWalkViewDelegate {
2620
+ public func walkViewEdgePadding(_ walkView: AMapNaviWalkView) -> UIEdgeInsets {
2621
+ driveViewEdgePadding
2622
+ }
2623
+
2624
+ public func walkView(_ walkView: AMapNaviWalkView, didChange showMode: AMapNaviWalkViewShowMode) {
2625
+ if showMode == .overview {
2626
+ scheduleOverviewRouteVisibleRegionRefresh()
2627
+ }
2628
+ }
2629
+ }
2630
+
2631
+ extension ExpoGaodeMapNaviView: AMapNaviWalkDataRepresentable {
2632
+ public func walkManager(_ walkManager: AMapNaviWalkManager, update naviRoute: AMapNaviRoute?) {
2633
+ currentNaviRoute = naviRoute
2634
+ if let routeLength = naviRoute?.routeLength, routeLength > 0 {
2635
+ currentRouteTotalLength = routeLength
2636
+ }
2637
+ }
2638
+
2639
+ public func walkManager(_ walkManager: AMapNaviWalkManager, update naviLocation: AMapNaviLocation?) {
2640
+ guard let naviLocation else {
2641
+ return
2642
+ }
2643
+ handleNavigationLocationUpdate(naviLocation)
2644
+ }
2645
+
2646
+ public func walkManager(_ walkManager: AMapNaviWalkManager, update naviInfo: AMapNaviInfo?) {
2647
+ guard let naviInfo else {
2648
+ return
2649
+ }
2650
+ handleNavigationInfoUpdate(naviInfo)
2651
+ }
2652
+ }
2653
+
2654
+ // MARK: - AMapNaviRideManagerDelegate
2655
+ extension ExpoGaodeMapNaviView: AMapNaviRideManagerDelegate {
2656
+ public func rideManager(_ rideManager: AMapNaviRideManager, didStartNavi naviMode: AMapNaviMode) {
2657
+ handleNavigationStarted(naviMode)
2658
+ }
2659
+
2660
+ public func rideManagerDidEndEmulatorNavi(_ rideManager: AMapNaviRideManager) {
2661
+ handleDidEndEmulatorNavi(reason: "ride_did_end_emulator_navi")
2662
+ }
2663
+
2664
+ public func rideManager(_ rideManager: AMapNaviRideManager, playNaviSound soundString: String, soundStringType: AMapNaviSoundType) {
2665
+ handleNavigationSound(soundString, soundStringType: soundStringType)
2666
+ }
2667
+
2668
+ public func rideManager(onArrivedDestination rideManager: AMapNaviRideManager) {
2669
+ handleArrivedDestination(source: "rideManagerOnArrivedDestination")
2670
+ deactivateNavigationAudioSessionIfNeeded(reason: "ride_arrived_destination")
2671
+ }
2672
+
2673
+ public func rideManagerNeedRecalculateRoute(forYaw rideManager: AMapNaviRideManager) {
2674
+ onRouteRecalculate([
2675
+ "reason": "yaw"
2676
+ ])
2677
+ }
2678
+
2679
+ public func rideManager(_ rideManager: AMapNaviRideManager, update gpsSignalStrength: AMapNaviGPSSignalStrength) {
2680
+ handleGpsSignalUpdate(gpsSignalStrength)
2681
+ }
2682
+
2683
+ public func rideManager(_ rideManager: AMapNaviRideManager, onArrivedWayPoint wayPointIndex: Int32) {
2684
+ handleWayPointArrived(index: Int(wayPointIndex))
2685
+ }
2686
+
2687
+ public func rideManager(_ rideManager: AMapNaviRideManager, onCalculateRouteFailure error: Error) {
2688
+ onRouteCalculated([
2689
+ "success": false,
2690
+ "errorInfo": error.localizedDescription
2691
+ ])
2692
+ }
2693
+ }
2694
+
2695
+ // MARK: - AMapNaviRideViewDelegate
2696
+ extension ExpoGaodeMapNaviView: AMapNaviRideViewDelegate {
2697
+ public func rideViewEdgePadding(_ rideView: AMapNaviRideView) -> UIEdgeInsets {
2698
+ driveViewEdgePadding
2699
+ }
2700
+
2701
+ public func rideView(_ rideView: AMapNaviRideView, didChange showMode: AMapNaviRideViewShowMode) {
2702
+ if showMode == .overview {
2703
+ scheduleOverviewRouteVisibleRegionRefresh()
2704
+ }
2705
+ }
2706
+ }
2707
+
2708
+ extension ExpoGaodeMapNaviView: AMapNaviRideDataRepresentable {
2709
+ public func rideManager(_ rideManager: AMapNaviRideManager, update naviRoute: AMapNaviRoute?) {
2710
+ currentNaviRoute = naviRoute
2711
+ if let routeLength = naviRoute?.routeLength, routeLength > 0 {
2712
+ currentRouteTotalLength = routeLength
2713
+ }
2714
+ }
2715
+
2716
+ public func rideManager(_ rideManager: AMapNaviRideManager, update naviLocation: AMapNaviLocation?) {
2717
+ guard let naviLocation else {
2718
+ return
2719
+ }
2720
+ handleNavigationLocationUpdate(naviLocation)
2721
+ }
2722
+
2723
+ public func rideManager(_ rideManager: AMapNaviRideManager, update naviInfo: AMapNaviInfo?) {
2724
+ guard let naviInfo else {
2725
+ return
2726
+ }
2727
+ handleNavigationInfoUpdate(naviInfo)
598
2728
  }
599
2729
  }