expo-gaode-map-navigation 2.0.12 → 2.0.14

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 (71) hide show
  1. package/README.md +29 -16
  2. package/android/build.gradle +8 -4
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +83 -15
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +13 -3
  6. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +36 -39
  7. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +5 -2
  8. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +122 -10
  9. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +2 -2
  10. package/android/src/main/java/expo/modules/gaodemap/map/search/ExpoGaodeMapSearchModule.kt +751 -0
  11. package/build/index.d.ts +26 -126
  12. package/build/index.d.ts.map +1 -1
  13. package/build/index.js +11 -612
  14. package/build/index.js.map +1 -1
  15. package/build/map/ExpoGaodeMapOfflineModule.d.ts +5 -0
  16. package/build/map/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  17. package/build/map/ExpoGaodeMapOfflineModule.js.map +1 -1
  18. package/build/map/components/overlays/HeatMap.d.ts.map +1 -1
  19. package/build/map/components/overlays/HeatMap.js +21 -2
  20. package/build/map/components/overlays/HeatMap.js.map +1 -1
  21. package/build/map/index.d.ts +3 -0
  22. package/build/map/index.d.ts.map +1 -1
  23. package/build/map/index.js +3 -0
  24. package/build/map/index.js.map +1 -1
  25. package/build/map/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  26. package/build/map/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  27. package/build/map/search/ExpoGaodeMapSearch.types.js +19 -0
  28. package/build/map/search/ExpoGaodeMapSearch.types.js.map +1 -0
  29. package/build/map/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  30. package/build/map/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  31. package/build/map/search/ExpoGaodeMapSearchModule.js +47 -0
  32. package/build/map/search/ExpoGaodeMapSearchModule.js.map +1 -0
  33. package/build/map/search/index.d.ts +156 -0
  34. package/build/map/search/index.d.ts.map +1 -0
  35. package/build/map/search/index.js +171 -0
  36. package/build/map/search/index.js.map +1 -0
  37. package/build/map/types/map-view.types.d.ts +4 -2
  38. package/build/map/types/map-view.types.d.ts.map +1 -1
  39. package/build/map/types/map-view.types.js.map +1 -1
  40. package/build/map/utils/ErrorHandler.js +11 -11
  41. package/build/map/utils/ErrorHandler.js.map +1 -1
  42. package/build/map/utils/OfflineMapManager.d.ts +4 -0
  43. package/build/map/utils/OfflineMapManager.d.ts.map +1 -1
  44. package/build/map/utils/OfflineMapManager.js +6 -0
  45. package/build/map/utils/OfflineMapManager.js.map +1 -1
  46. package/build/route-geometry.d.ts +13 -0
  47. package/build/route-geometry.d.ts.map +1 -0
  48. package/build/route-geometry.js +154 -0
  49. package/build/route-geometry.js.map +1 -0
  50. package/build/route-planning.d.ts +21 -0
  51. package/build/route-planning.d.ts.map +1 -0
  52. package/build/route-planning.js +67 -0
  53. package/build/route-planning.js.map +1 -0
  54. package/build/web-api-fallback.d.ts +5 -0
  55. package/build/web-api-fallback.d.ts.map +1 -0
  56. package/build/web-api-fallback.js +160 -0
  57. package/build/web-api-fallback.js.map +1 -0
  58. package/build/web-route-following.d.ts +3 -0
  59. package/build/web-route-following.d.ts.map +1 -0
  60. package/build/web-route-following.js +178 -0
  61. package/build/web-route-following.js.map +1 -0
  62. package/expo-module.config.json +4 -2
  63. package/ios/ExpoGaodeMapNaviView.swift +16 -17
  64. package/ios/ExpoGaodeMapNavigation.podspec +2 -1
  65. package/ios/map/ExpoGaodeMapOfflineModule.swift +61 -0
  66. package/ios/map/ExpoGaodeMapSearchModule.swift +773 -0
  67. package/ios/map/modules/LocationManager.swift +9 -3
  68. package/ios/map/overlays/PolylineView.swift +6 -12
  69. package/package.json +2 -2
  70. package/plugin/build/withGaodeMap.js +12 -0
  71. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/RouteCalculator.kt +0 -173
@@ -0,0 +1,178 @@
1
+ import ExpoGaodeMapNavigationModule from './ExpoGaodeMapNavigationModule';
2
+ import { buildAnchorWaypointsFromWebRoute, calculatePathLengthSafe, dedupeAdjacentPoints, getDistanceToPathSafe, normalizeWebRoutePolyline, samplePolyline } from './route-geometry';
3
+ function extractRoutePolyline(route) {
4
+ if (Array.isArray(route.polyline) && route.polyline.length > 0) {
5
+ return route.polyline;
6
+ }
7
+ const segments = Array.isArray(route.segments) ? route.segments : [];
8
+ if (segments.length > 0) {
9
+ return dedupeAdjacentPoints(segments.flatMap((segment) => segment.polyline ?? []));
10
+ }
11
+ const steps = Array.isArray(route.steps) ? route.steps : [];
12
+ if (steps.length > 0) {
13
+ return dedupeAdjacentPoints(steps.flatMap((step) => step.polyline ?? []));
14
+ }
15
+ return [];
16
+ }
17
+ function resolveIndependentRouteId(result, route, routeIndex) {
18
+ if (typeof route.id === 'number') {
19
+ return route.id;
20
+ }
21
+ if (typeof route.routeId === 'number') {
22
+ return route.routeId;
23
+ }
24
+ return result.routeIds?.[routeIndex];
25
+ }
26
+ function scoreIndependentRouteAgainstWebPolyline(result, route, routeIndex, webPolyline, anchorWaypoints, thresholdMeters) {
27
+ // 评分思路很简单:看原生独立路线与 Web 折线有多接近,
28
+ // 再把平均偏差、最大偏差、漏掉的锚点一起折成一个排序分数。
29
+ const nativePolyline = extractRoutePolyline(route);
30
+ if (nativePolyline.length === 0 || webPolyline.length === 0) {
31
+ return null;
32
+ }
33
+ const sampledNativePoints = samplePolyline(nativePolyline);
34
+ const pointDistances = sampledNativePoints.map((point) => getDistanceToPathSafe(webPolyline, point));
35
+ const averageDeviationMeters = pointDistances.reduce((total, distance) => total + distance, 0) / pointDistances.length;
36
+ const maxDeviationMeters = Math.max(...pointDistances);
37
+ const missedAnchorCount = anchorWaypoints.reduce((count, point) => (getDistanceToPathSafe(nativePolyline, point) > thresholdMeters ? count + 1 : count), 0);
38
+ return {
39
+ routeId: resolveIndependentRouteId(result, route, routeIndex),
40
+ routeIndex,
41
+ averageDeviationMeters,
42
+ maxDeviationMeters,
43
+ missedAnchorCount,
44
+ score: averageDeviationMeters +
45
+ maxDeviationMeters * 0.35 +
46
+ missedAnchorCount * thresholdMeters * 0.5,
47
+ };
48
+ }
49
+ function evaluateIndependentResultAgainstWebRoute(independentResult, webPolyline, anchorWaypoints, maxDeviationMeters) {
50
+ // 先给每条原生候选路线打分,再把最接近的一条拿出来判断是 matched / approximate / preview_only。
51
+ const candidateMatches = independentResult.routes
52
+ .map((route, routeIndex) => scoreIndependentRouteAgainstWebPolyline(independentResult, route, routeIndex, webPolyline, anchorWaypoints, maxDeviationMeters))
53
+ .filter((candidate) => candidate !== null)
54
+ .sort((routeA, routeB) => routeA.score - routeB.score);
55
+ const bestMatch = candidateMatches[0];
56
+ const selectedRoute = bestMatch
57
+ ? independentResult.routes[bestMatch.routeIndex]
58
+ : undefined;
59
+ const nativePolyline = selectedRoute ? extractRoutePolyline(selectedRoute) : [];
60
+ let mode = 'preview_only';
61
+ let reason = '未找到足够接近 Web 规划线的原生路线';
62
+ if (bestMatch) {
63
+ if (bestMatch.averageDeviationMeters <= maxDeviationMeters / 2 &&
64
+ bestMatch.maxDeviationMeters <= maxDeviationMeters &&
65
+ bestMatch.missedAnchorCount === 0) {
66
+ mode = 'matched';
67
+ reason = '原生路线与 Web 规划线高度接近,可直接按近似结果导航';
68
+ }
69
+ else if (bestMatch.averageDeviationMeters <= maxDeviationMeters &&
70
+ bestMatch.maxDeviationMeters <= maxDeviationMeters * 2) {
71
+ mode = 'approximate';
72
+ reason = '原生路线与 Web 规划线接近,但仍存在可见偏差';
73
+ }
74
+ }
75
+ return {
76
+ candidateMatches,
77
+ bestMatch,
78
+ selectedRoute,
79
+ nativePolyline,
80
+ mode,
81
+ reason,
82
+ };
83
+ }
84
+ async function runIndependentDriveRoute(options) {
85
+ return ExpoGaodeMapNavigationModule.independentDriveRoute(options);
86
+ }
87
+ export async function followWebPlannedRoute(options) {
88
+ const { from, to, webRoute, strategy, carNumber, restriction, maxDeviationMeters = 120, startNavigation = false, naviType = 0, } = options;
89
+ const webPolyline = normalizeWebRoutePolyline(webRoute);
90
+ if (webPolyline.length < 2) {
91
+ throw new Error('webRoute.polyline 至少需要 2 个点');
92
+ }
93
+ // 第一步:从 Web 线路提取少量锚点,让原生独立算路先尽量往这条线靠。
94
+ const anchorWaypoints = buildAnchorWaypointsFromWebRoute(options);
95
+ const anchoredIndependentResult = await runIndependentDriveRoute({
96
+ from,
97
+ to,
98
+ strategy,
99
+ carNumber,
100
+ restriction,
101
+ waypoints: anchorWaypoints,
102
+ });
103
+ let independentResult = anchoredIndependentResult;
104
+ let evaluation = evaluateIndependentResultAgainstWebRoute(anchoredIndependentResult, webPolyline, anchorWaypoints, maxDeviationMeters);
105
+ let navigationUsesAnchorWaypoints = anchorWaypoints.length > 0;
106
+ // 如果锚点路线已经足够接近,再尝试去掉锚点重算一次。
107
+ // 这样可以避免“为了贴线而被锚点拖偏”的情况。
108
+ if (evaluation.bestMatch && evaluation.mode !== 'preview_only' && anchorWaypoints.length > 0) {
109
+ try {
110
+ const directIndependentResult = await runIndependentDriveRoute({
111
+ from,
112
+ to,
113
+ strategy,
114
+ carNumber,
115
+ restriction,
116
+ });
117
+ const directEvaluation = evaluateIndependentResultAgainstWebRoute(directIndependentResult, webPolyline, [], maxDeviationMeters);
118
+ const anchoredBest = evaluation.bestMatch;
119
+ const directBest = directEvaluation.bestMatch;
120
+ const canSwitchToDirectNavigation = Boolean(directBest) &&
121
+ directEvaluation.mode !== 'preview_only' &&
122
+ directBest.averageDeviationMeters <= Math.max(anchoredBest.averageDeviationMeters + 45, anchoredBest.averageDeviationMeters * 1.45) &&
123
+ directBest.maxDeviationMeters <= Math.max(anchoredBest.maxDeviationMeters + 90, anchoredBest.maxDeviationMeters * 1.45);
124
+ if (canSwitchToDirectNavigation) {
125
+ // 直连结果更自然,就切到直连结果,并清掉刚才的锚点算路缓存。
126
+ ExpoGaodeMapNavigationModule.clearIndependentRoute({
127
+ token: anchoredIndependentResult.token,
128
+ }).catch(() => { });
129
+ independentResult = directIndependentResult;
130
+ evaluation = directEvaluation;
131
+ navigationUsesAnchorWaypoints = false;
132
+ evaluation.reason =
133
+ directEvaluation.mode === 'matched'
134
+ ? '已切换为无途经点导航结果,且与 Web 规划线高度接近'
135
+ : '已切换为无途经点导航结果,但与 Web 规划线仍存在轻微偏差';
136
+ }
137
+ else {
138
+ // 直连结果不如锚点方案,就保留锚点方案作为最终导航依据。
139
+ ExpoGaodeMapNavigationModule.clearIndependentRoute({
140
+ token: directIndependentResult.token,
141
+ }).catch(() => { });
142
+ evaluation.reason = `${evaluation.reason};最终导航仍需依赖锚点途经点逼近 Web 线路`;
143
+ }
144
+ }
145
+ catch {
146
+ evaluation.reason = `${evaluation.reason};无途经点重算失败,最终导航仍需依赖锚点途经点`;
147
+ }
148
+ }
149
+ let navigationStarted = false;
150
+ if (startNavigation && evaluation.bestMatch && evaluation.mode !== 'preview_only') {
151
+ // 只有当评估结果足够接近时才真正启动导航,避免把偏差过大的结果直接交给导航 SDK。
152
+ navigationStarted = await ExpoGaodeMapNavigationModule.startNaviWithIndependentPath({
153
+ token: independentResult.token,
154
+ naviType,
155
+ routeId: evaluation.bestMatch.routeId,
156
+ routeIndex: evaluation.bestMatch.routeId == null ? evaluation.bestMatch.routeIndex : undefined,
157
+ });
158
+ }
159
+ return {
160
+ mode: evaluation.mode,
161
+ token: independentResult.token,
162
+ anchorWaypoints,
163
+ webDistance: calculatePathLengthSafe(webPolyline),
164
+ nativeDistance: evaluation.nativePolyline.length > 1
165
+ ? calculatePathLengthSafe(evaluation.nativePolyline)
166
+ : undefined,
167
+ selectedRouteId: evaluation.bestMatch?.routeId,
168
+ selectedRouteIndex: evaluation.bestMatch?.routeIndex,
169
+ averageDeviationMeters: evaluation.bestMatch?.averageDeviationMeters,
170
+ maxDeviationMeters: evaluation.bestMatch?.maxDeviationMeters,
171
+ navigationStarted,
172
+ navigationUsesAnchorWaypoints,
173
+ independentResult,
174
+ candidateMatches: evaluation.candidateMatches,
175
+ reason: evaluation.reason,
176
+ };
177
+ }
178
+ //# sourceMappingURL=web-route-following.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-route-following.js","sourceRoot":"","sources":["../src/web-route-following.ts"],"names":[],"mappings":"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","sourcesContent":["import ExpoGaodeMapNavigationModule from './ExpoGaodeMapNavigationModule';\nimport { buildAnchorWaypointsFromWebRoute, calculatePathLengthSafe, dedupeAdjacentPoints, getDistanceToPathSafe, normalizeWebRoutePolyline, samplePolyline } from './route-geometry';\nimport type {\n FollowWebPlannedRouteCandidate,\n FollowWebPlannedRouteOptions,\n FollowWebPlannedRouteResult,\n IndependentRouteResult,\n NaviPoint,\n RouteResult,\n} from './types';\n\ntype RouteLike = RouteResult & {\n routeId?: number;\n steps?: Array<{ polyline?: NaviPoint[] }>;\n};\n\nfunction extractRoutePolyline(route: RouteLike): NaviPoint[] {\n if (Array.isArray(route.polyline) && route.polyline.length > 0) {\n return route.polyline;\n }\n\n const segments = Array.isArray(route.segments) ? route.segments : [];\n if (segments.length > 0) {\n return dedupeAdjacentPoints(\n segments.flatMap((segment) => segment.polyline ?? [])\n );\n }\n\n const steps = Array.isArray(route.steps) ? route.steps : [];\n if (steps.length > 0) {\n return dedupeAdjacentPoints(\n steps.flatMap((step) => step.polyline ?? [])\n );\n }\n\n return [];\n}\n\nfunction resolveIndependentRouteId(\n result: IndependentRouteResult,\n route: RouteLike,\n routeIndex: number\n): number | undefined {\n if (typeof route.id === 'number') {\n return route.id;\n }\n if (typeof route.routeId === 'number') {\n return route.routeId;\n }\n return result.routeIds?.[routeIndex];\n}\n\nfunction scoreIndependentRouteAgainstWebPolyline(\n result: IndependentRouteResult,\n route: RouteLike,\n routeIndex: number,\n webPolyline: NaviPoint[],\n anchorWaypoints: NaviPoint[],\n thresholdMeters: number\n): FollowWebPlannedRouteCandidate | null {\n // 评分思路很简单:看原生独立路线与 Web 折线有多接近,\n // 再把平均偏差、最大偏差、漏掉的锚点一起折成一个排序分数。\n const nativePolyline = extractRoutePolyline(route);\n if (nativePolyline.length === 0 || webPolyline.length === 0) {\n return null;\n }\n\n const sampledNativePoints = samplePolyline(nativePolyline);\n const pointDistances = sampledNativePoints.map((point) =>\n getDistanceToPathSafe(webPolyline, point)\n );\n const averageDeviationMeters =\n pointDistances.reduce((total, distance) => total + distance, 0) / pointDistances.length;\n const maxDeviationMeters = Math.max(...pointDistances);\n\n const missedAnchorCount = anchorWaypoints.reduce((count, point) => (\n getDistanceToPathSafe(nativePolyline, point) > thresholdMeters ? count + 1 : count\n ), 0);\n\n return {\n routeId: resolveIndependentRouteId(result, route, routeIndex),\n routeIndex,\n averageDeviationMeters,\n maxDeviationMeters,\n missedAnchorCount,\n score:\n averageDeviationMeters +\n maxDeviationMeters * 0.35 +\n missedAnchorCount * thresholdMeters * 0.5,\n };\n}\n\nfunction evaluateIndependentResultAgainstWebRoute(\n independentResult: IndependentRouteResult,\n webPolyline: NaviPoint[],\n anchorWaypoints: NaviPoint[],\n maxDeviationMeters: number\n) {\n // 先给每条原生候选路线打分,再把最接近的一条拿出来判断是 matched / approximate / preview_only。\n const candidateMatches = independentResult.routes\n .map((route, routeIndex) =>\n scoreIndependentRouteAgainstWebPolyline(\n independentResult,\n route as RouteLike,\n routeIndex,\n webPolyline,\n anchorWaypoints,\n maxDeviationMeters\n )\n )\n .filter((candidate): candidate is FollowWebPlannedRouteCandidate => candidate !== null)\n .sort((routeA, routeB) => routeA.score - routeB.score);\n\n const bestMatch = candidateMatches[0];\n const selectedRoute = bestMatch\n ? independentResult.routes[bestMatch.routeIndex] as RouteLike\n : undefined;\n const nativePolyline = selectedRoute ? extractRoutePolyline(selectedRoute) : [];\n\n let mode: FollowWebPlannedRouteResult['mode'] = 'preview_only';\n let reason = '未找到足够接近 Web 规划线的原生路线';\n\n if (bestMatch) {\n if (\n bestMatch.averageDeviationMeters <= maxDeviationMeters / 2 &&\n bestMatch.maxDeviationMeters <= maxDeviationMeters &&\n bestMatch.missedAnchorCount === 0\n ) {\n mode = 'matched';\n reason = '原生路线与 Web 规划线高度接近,可直接按近似结果导航';\n } else if (\n bestMatch.averageDeviationMeters <= maxDeviationMeters &&\n bestMatch.maxDeviationMeters <= maxDeviationMeters * 2\n ) {\n mode = 'approximate';\n reason = '原生路线与 Web 规划线接近,但仍存在可见偏差';\n }\n }\n\n return {\n candidateMatches,\n bestMatch,\n selectedRoute,\n nativePolyline,\n mode,\n reason,\n };\n}\n\nasync function runIndependentDriveRoute(options: {\n from: FollowWebPlannedRouteOptions['from'];\n to: FollowWebPlannedRouteOptions['to'];\n strategy?: FollowWebPlannedRouteOptions['strategy'];\n carNumber?: FollowWebPlannedRouteOptions['carNumber'];\n restriction?: FollowWebPlannedRouteOptions['restriction'];\n waypoints?: NaviPoint[];\n}): Promise<IndependentRouteResult> {\n return ExpoGaodeMapNavigationModule.independentDriveRoute(options);\n}\n\nexport async function followWebPlannedRoute(\n options: FollowWebPlannedRouteOptions\n): Promise<FollowWebPlannedRouteResult> {\n const {\n from,\n to,\n webRoute,\n strategy,\n carNumber,\n restriction,\n maxDeviationMeters = 120,\n startNavigation = false,\n naviType = 0,\n } = options;\n\n const webPolyline = normalizeWebRoutePolyline(webRoute);\n if (webPolyline.length < 2) {\n throw new Error('webRoute.polyline 至少需要 2 个点');\n }\n\n // 第一步:从 Web 线路提取少量锚点,让原生独立算路先尽量往这条线靠。\n const anchorWaypoints = buildAnchorWaypointsFromWebRoute(options);\n const anchoredIndependentResult = await runIndependentDriveRoute({\n from,\n to,\n strategy,\n carNumber,\n restriction,\n waypoints: anchorWaypoints,\n });\n\n let independentResult = anchoredIndependentResult;\n let evaluation = evaluateIndependentResultAgainstWebRoute(\n anchoredIndependentResult,\n webPolyline,\n anchorWaypoints,\n maxDeviationMeters\n );\n let navigationUsesAnchorWaypoints = anchorWaypoints.length > 0;\n\n // 如果锚点路线已经足够接近,再尝试去掉锚点重算一次。\n // 这样可以避免“为了贴线而被锚点拖偏”的情况。\n if (evaluation.bestMatch && evaluation.mode !== 'preview_only' && anchorWaypoints.length > 0) {\n try {\n const directIndependentResult = await runIndependentDriveRoute({\n from,\n to,\n strategy,\n carNumber,\n restriction,\n });\n\n const directEvaluation = evaluateIndependentResultAgainstWebRoute(\n directIndependentResult,\n webPolyline,\n [],\n maxDeviationMeters\n );\n const anchoredBest = evaluation.bestMatch;\n const directBest = directEvaluation.bestMatch;\n\n const canSwitchToDirectNavigation =\n Boolean(directBest) &&\n directEvaluation.mode !== 'preview_only' &&\n directBest!.averageDeviationMeters <= Math.max(\n anchoredBest.averageDeviationMeters + 45,\n anchoredBest.averageDeviationMeters * 1.45\n ) &&\n directBest!.maxDeviationMeters <= Math.max(\n anchoredBest.maxDeviationMeters + 90,\n anchoredBest.maxDeviationMeters * 1.45\n );\n\n if (canSwitchToDirectNavigation) {\n // 直连结果更自然,就切到直连结果,并清掉刚才的锚点算路缓存。\n ExpoGaodeMapNavigationModule.clearIndependentRoute({\n token: anchoredIndependentResult.token,\n }).catch(() => {});\n independentResult = directIndependentResult;\n evaluation = directEvaluation;\n navigationUsesAnchorWaypoints = false;\n evaluation.reason =\n directEvaluation.mode === 'matched'\n ? '已切换为无途经点导航结果,且与 Web 规划线高度接近'\n : '已切换为无途经点导航结果,但与 Web 规划线仍存在轻微偏差';\n } else {\n // 直连结果不如锚点方案,就保留锚点方案作为最终导航依据。\n ExpoGaodeMapNavigationModule.clearIndependentRoute({\n token: directIndependentResult.token,\n }).catch(() => {});\n evaluation.reason = `${evaluation.reason};最终导航仍需依赖锚点途经点逼近 Web 线路`;\n }\n } catch {\n evaluation.reason = `${evaluation.reason};无途经点重算失败,最终导航仍需依赖锚点途经点`;\n }\n }\n\n let navigationStarted = false;\n if (startNavigation && evaluation.bestMatch && evaluation.mode !== 'preview_only') {\n // 只有当评估结果足够接近时才真正启动导航,避免把偏差过大的结果直接交给导航 SDK。\n navigationStarted = await ExpoGaodeMapNavigationModule.startNaviWithIndependentPath({\n token: independentResult.token,\n naviType,\n routeId: evaluation.bestMatch.routeId,\n routeIndex:\n evaluation.bestMatch.routeId == null ? evaluation.bestMatch.routeIndex : undefined,\n });\n }\n\n return {\n mode: evaluation.mode,\n token: independentResult.token,\n anchorWaypoints,\n webDistance: calculatePathLengthSafe(webPolyline),\n nativeDistance:\n evaluation.nativePolyline.length > 1\n ? calculatePathLengthSafe(evaluation.nativePolyline)\n : undefined,\n selectedRouteId: evaluation.bestMatch?.routeId,\n selectedRouteIndex: evaluation.bestMatch?.routeIndex,\n averageDeviationMeters: evaluation.bestMatch?.averageDeviationMeters,\n maxDeviationMeters: evaluation.bestMatch?.maxDeviationMeters,\n navigationStarted,\n navigationUsesAnchorWaypoints,\n independentResult,\n candidateMatches: evaluation.candidateMatches,\n reason: evaluation.reason,\n };\n}\n"]}
@@ -14,7 +14,8 @@
14
14
  "MultiPointViewModule",
15
15
  "HeatMapViewModule",
16
16
  "ClusterViewModule",
17
- "ExpoGaodeMapOfflineModule"
17
+ "ExpoGaodeMapOfflineModule",
18
+ "ExpoGaodeMapSearchModule"
18
19
  ]
19
20
  },
20
21
  "android": {
@@ -30,7 +31,8 @@
30
31
  "expo.modules.gaodemap.map.overlays.MultiPointViewModule",
31
32
  "expo.modules.gaodemap.map.overlays.HeatMapViewModule",
32
33
  "expo.modules.gaodemap.map.overlays.ClusterViewModule",
33
- "expo.modules.gaodemap.map.ExpoGaodeMapOfflineModule"
34
+ "expo.modules.gaodemap.map.ExpoGaodeMapOfflineModule",
35
+ "expo.modules.gaodemap.map.search.ExpoGaodeMapSearchModule"
34
36
  ]
35
37
  }
36
38
  }
@@ -442,6 +442,7 @@ public class ExpoGaodeMapNaviView: ExpoView {
442
442
  private var hasLoggedMissingBackgroundAudioMode: Bool = false
443
443
  private var renderedCustomWaypointAnnotations: [AMapNaviCompositeCustomAnnotation] = []
444
444
  private var customWaypointMarkers: [NaviCustomWaypointMarkerModel] = []
445
+ private let customSpeechSynthesizer = AVSpeechSynthesizer()
445
446
 
446
447
  private enum LaneStringKind {
447
448
  case background
@@ -1231,10 +1232,6 @@ public class ExpoGaodeMapNaviView: ExpoView {
1231
1232
  }
1232
1233
 
1233
1234
  private func resolveTravelTurnIconImage(iconType: Int) -> UIImage? {
1234
- let iconEnum = AMapNaviIconType(rawValue: iconType) ?? .none
1235
- if activeScene == .ride {
1236
- return AMapNaviRideView.rideViewTurnIconImage(with: iconEnum)
1237
- }
1238
1235
  return fallbackTurnIconImage(iconType: iconType)
1239
1236
  }
1240
1237
 
@@ -1637,11 +1634,7 @@ public class ExpoGaodeMapNaviView: ExpoView {
1637
1634
  let resizedImage = self?.resizeImageIfNeeded(image, targetSize: self?.carImageSize) ?? image
1638
1635
  driveView?.setCarImage(resizedImage)
1639
1636
  walkView?.setCarImage(resizedImage)
1640
- if let resizedImage {
1641
- rideView?.setCarImageWithSize(resizedImage)
1642
- } else {
1643
- rideView?.setCarImage(nil)
1644
- }
1637
+ rideView?.setCarImage(resizedImage)
1645
1638
  }
1646
1639
  }
1647
1640
 
@@ -2323,18 +2316,24 @@ public class ExpoGaodeMapNaviView: ExpoView {
2323
2316
  }
2324
2317
 
2325
2318
  func playCustomTTS(text: String, forcePlay: Bool, promise: Promise) {
2326
- guard let manager = currentNaviManager() else {
2319
+ guard currentNaviManager() != nil else {
2327
2320
  promise.reject("NAVI_MANAGER_UNAVAILABLE", "导航管理器尚未初始化")
2328
2321
  return
2329
2322
  }
2330
- let success = manager.playTTS(text, forcePlay: forcePlay)
2331
- if success {
2332
- promise.resolve([
2333
- "success": true
2334
- ])
2335
- } else {
2336
- promise.reject("PLAY_TTS_FAILED", "当前场景暂不支持或正在播报其他导航语音")
2323
+
2324
+ if forcePlay {
2325
+ customSpeechSynthesizer.stopSpeaking(at: .immediate)
2326
+ } else if customSpeechSynthesizer.isSpeaking {
2327
+ promise.reject("PLAY_TTS_FAILED", "当前正在播报其他导航语音")
2328
+ return
2337
2329
  }
2330
+
2331
+ let utterance = AVSpeechUtterance(string: text)
2332
+ utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
2333
+ customSpeechSynthesizer.speak(utterance)
2334
+ promise.resolve([
2335
+ "success": true
2336
+ ])
2338
2337
  }
2339
2338
 
2340
2339
  // MARK: - Lifecycle
@@ -24,9 +24,10 @@ Pod::Spec.new do |s|
24
24
  s.static_framework = true
25
25
 
26
26
  s.dependency 'ExpoModulesCore'
27
- s.dependency 'AMapNavi'
27
+ s.dependency 'AMapNavi','= 11.1.200'
28
28
  s.dependency 'AMapFoundation'
29
29
  s.dependency 'AMapLocation'
30
+ s.dependency 'AMapSearch'
30
31
 
31
32
  s.library = 'c++'
32
33
 
@@ -2,6 +2,8 @@ import ExpoModulesCore
2
2
  import AMapFoundationKit
3
3
  import AMapNaviKit
4
4
  import Foundation
5
+ import AMapNaviKit
6
+ import UIKit
5
7
 
6
8
  /**
7
9
  * 高德地图离线地图模块 (iOS)
@@ -66,6 +68,12 @@ public class ExpoGaodeMapOfflineModule: Module {
66
68
  self.downloadingCities.removeAll()
67
69
  self.pausedCities.removeAll()
68
70
  }
71
+
72
+ AsyncFunction("openOfflineMapUI") { (promise: Promise) in
73
+ DispatchQueue.main.async {
74
+ self.openOfflineMapUI(promise: promise)
75
+ }
76
+ }
69
77
 
70
78
  // ==================== 1. 地图列表管理 ====================
71
79
 
@@ -383,6 +391,59 @@ public class ExpoGaodeMapOfflineModule: Module {
383
391
 
384
392
  return offlineMapManager
385
393
  }
394
+
395
+ private func openOfflineMapUI(promise: Promise) {
396
+ guard getOfflineMapManager() != nil else {
397
+ promise.reject("ERR_SETUP", "Setup failed")
398
+ return
399
+ }
400
+
401
+ guard let detailViewController = MAOfflineMapViewController.sharedInstance() else {
402
+ promise.reject("ERR_OFFLINE_MAP_UI", "Offline map UI is unavailable")
403
+ return
404
+ }
405
+
406
+ guard let presenter = currentPresenterViewController() else {
407
+ promise.reject("ERR_NO_VIEW_CONTROLLER", "No active view controller")
408
+ return
409
+ }
410
+
411
+ if let navigationController = presenter as? UINavigationController {
412
+ navigationController.pushViewController(detailViewController, animated: true)
413
+ } else if let navigationController = presenter.navigationController {
414
+ navigationController.pushViewController(detailViewController, animated: true)
415
+ } else {
416
+ let navigationController = UINavigationController(rootViewController: detailViewController)
417
+ presenter.present(navigationController, animated: true)
418
+ }
419
+ promise.resolve(nil)
420
+ }
421
+
422
+ private func currentPresenterViewController() -> UIViewController? {
423
+ guard let scene = UIApplication.shared.connectedScenes
424
+ .compactMap({ $0 as? UIWindowScene })
425
+ .first(where: { $0.activationState == .foregroundActive }) else {
426
+ return topViewController(from: UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController)
427
+ }
428
+
429
+ let root = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController
430
+ ?? scene.windows.first?.rootViewController
431
+ return topViewController(from: root)
432
+ }
433
+
434
+ private func topViewController(from root: UIViewController?) -> UIViewController? {
435
+ guard let root else { return nil }
436
+ if let presented = root.presentedViewController {
437
+ return topViewController(from: presented)
438
+ }
439
+ if let navigationController = root as? UINavigationController {
440
+ return topViewController(from: navigationController.visibleViewController)
441
+ }
442
+ if let tabBarController = root as? UITabBarController {
443
+ return topViewController(from: tabBarController.selectedViewController)
444
+ }
445
+ return root
446
+ }
386
447
 
387
448
  private func ensureSetup(completion: @escaping (Bool) -> Void) {
388
449
  guard let manager = getOfflineMapManager() else {