前幾天已經能判斷使用者是否走在推薦路線上,也能計算整條推薦路線的完成率。
接下來就要把這些路線狀態實際用在導航上,除了顯示已完成的推薦路段,也要告訴使用者下一個路口怎麼走,並在接近轉彎時播放語音提醒。

Google Routes 已經回傳完整的 Polyline,看起來只要把路線畫在地圖上就完成了。
但實際走在路上時,比起一直盯著地圖,大部分人其實更習慣看到「前方 25 公尺右轉」這種明確的導航資訊,甚至直接透過語音知道接下來該怎麼走。
尤其像上面這張圖,地圖底圖中的道路顏色和推薦路線很接近,需要仔細看才能分辨實際該走哪一條路,散步時其實非常不方便。
因此除了畫出路線之外,還需要把目前的散步狀態轉成更容易理解的導航資訊。
一開始我以為還要自己分析路線,例如根據前後幾個座標計算夾角,再判斷是左轉、右轉還是直走。
後來查看 Google Routes API 才發現,computeRoutes 回傳的不只是整條路線,也可以取得完整的導航步驟:
routes[].legs[].steps[]
每個 Step 都包含這一段的距離、Polyline,以及 navigationInstruction,其中又包含:
maneuver)instructions)例如:
{
"distanceMeters": 120,
"navigationInstruction": {
"maneuver": "TURN_RIGHT",
"instructions": "右轉進入河濱步道"
}
}
因此不需要再自己根據座標判斷轉彎方向,只要把 Google Routes 回傳的 Navigation Step 整理成 App 可以直接使用的導航資料即可。
不過 Google Routes API 會透過 Field Mask 決定要回傳哪些資料,如果原本只取得距離、時間和整條路線的 Polyline,還需要把 Step 相關欄位加入 Field Mask。
例如:
routes.legs.steps.distanceMeters
routes.legs.steps.polyline
routes.legs.steps.navigationInstruction
這樣才能取得後面導航需要使用的 Step 資訊。
目前已經知道哪些 Segment 已經完成,因此也可以直接把這個結果呈現在地圖上。
如果替每一個完成的 Segment 都建立一條 Polyline,一條長路線可能會產生數百個地圖元件,不但增加繪製負擔,也容易讓畫面看起來一段一段的。
因此我會先把連續完成的 Segment 合併,再一起繪製。
例如:
Segment 0 ✓
Segment 1 ✓
Segment 2 ✓
Segment 3 ✗
Segment 4 ✓
Segment 5 ✓
最後整理成:
Polyline 1
Segment 0 → Segment 2
Polyline 2
Segment 4 → Segment 5
程式中先取得合併後的 Polyline:
const coveredPolylines =
polylinesForCoveredSegments(
routeSegments,
routeProgress.coveredSegmentIndexes,
);
接著疊在原本的推薦路線上:
{/* 推薦路線 */}
<Polyline
coordinates={recommendedCoordinates}
strokeColor="#3E6FA6"
strokeWidth={6}
lineDashPattern={[12, 8]}
/>
{/* 已完成路段 */}
{coveredPolylines.map((coordinates, index) => (
<Polyline
key={index}
coordinates={coordinates}
strokeColor="#2E9B64"
strokeWidth={7}
/>
))}
畫面最後會有兩層:
隨著 GPS 持續更新,綠色路段也會一路延伸,這樣就能很直覺地看出哪些地方已經走過。
Google Routes 已經把整條路線拆成許多 Navigation Step,而前面也已經能將 GPS 匹配到推薦路線上的位置,因此接下來只要進一步對應到 Navigation Step,就能知道使用者目前走到哪一個導航步驟。
假設目前的路線是:
Step 0
沿中正路直走
Step 1
右轉進入河濱步道
如果使用者目前位於 Step 0,需要顯示的就是下一個 Step 的 navigationInstruction:
目前:Step 0
沿中正路直走
下一步:Step 1
右轉進入河濱步道
因此我會把 Google Routes 的資料整理成 App 內部使用的格式:
type NavigationGuidance = {
maneuver: Maneuver;
instruction: string;
distanceToManeuverMeters: number;
};
Google Routes 原本回傳的 TURN_RIGHT、TURN_LEFT 等 maneuver,也會在這一層轉成 App 內部統一使用的格式。
例如:
{
maneuver: "turn_right",
instruction: "右轉進入河濱步道",
distanceToManeuverMeters: 25,
}
畫面就可以直接呈現:
→ 右轉進入河濱步道
25 公尺
效果如下:

這樣就不需要一直研究地圖上的線,只要看導航提示,就能知道下一個路口該怎麼走。
有了轉彎提示之後,接下來還有一個問題:散步時不可能一直拿著手機盯著導航。
尤其在過馬路或欣賞風景時,手機可能大部分時間都放在口袋裡。
因此我另外加入 Text-to-Speech(TTS),讓 App 可以直接播報導航內容。
初始化後,在需要提醒時呼叫:
await Tts.speak("前方 30 公尺右轉");
一開始我只打算在轉角前提醒一次,但實際測試後發現,如果等到只剩幾公尺才播報,很多時候已經快走到路口,根本來不及反應。
因此目前先使用 30 公尺與 10 公尺作為兩個提醒門檻:
30 公尺提醒
↓
10 公尺提醒
↓
通過路口
↓
切換下一個 Step
30 公尺的提醒可以提前告訴使用者等等要轉彎,而 10 公尺則是在接近路口時再提醒一次。
不過 GPS 不一定剛好會取得 30 公尺或 10 公尺的位置。
例如第一次取得定位時,可能距離轉角就只剩 8 公尺。這種情況只需要播放 10 公尺的提醒,不需要把兩個提醒連續播一次。
因此判斷時會先從距離較近的門檻開始:
if (remaining <= 10) {
announceOnce(
`step:${guidance.sequence}:10`,
spokenInstruction(guidance),
);
} else if (remaining <= 30) {
announceOnce(
`step:${guidance.sequence}:30`,
spokenInstruction(guidance),
);
}
這樣即使第一次取得的位置已經非常靠近路口,也只會播放最適合當下距離的提醒。
GPS 每隔幾秒就會更新一次,如果只有判斷 remaining <= 30,那麼只要使用者待在 30 公尺範圍內,每次定位更新都會再次播放。
因此還需要記錄哪些提醒已經播過。
我會替每一次提醒建立一組唯一 Key,例如:
step:3:30
step:3:10
第一個數字代表 Step,第二個數字代表提醒距離。
所有已播放的提醒都放進一個 Set:
const announced = new Set<string>();
async function announceOnce(
key: string,
message: string,
) {
if (announced.has(key)) {
return;
}
announced.add(key);
await speechService.speak(message);
}
播放前先檢查 Key 是否已經存在,這樣每一個 Step 的 30 公尺與 10 公尺提醒最多只會播放一次。
不過 Set 只能存在記憶體裡。
如果 App 在背景被系統回收、使用者手動關閉 App,或因其他原因重新啟動,原本記錄的提醒狀態就會消失。
這樣重新進入散步畫面後,就有可能再次收到已經播放過的導航提醒。
因此我把目前的導航狀態另外保存到 SQLite,包括目前走到哪一個 Step,以及哪些提醒已經播放過:
await saveNavigationState({
walkId,
currentStepIndex,
announcedKeys: [...announced],
});
這些資料不需要每次 GPS 更新都寫入 SQLite,只要在 Navigation Step 切換,或新的語音提醒播放後更新即可。
重新開啟 App 時,再把狀態還原:
const state = await loadNavigationState(walkId);
currentStepIndex = state?.currentStepIndex ?? 0;
announced = new Set(state?.announcedKeys ?? []);
這樣即使散步途中 App 被關閉或重新啟動,導航仍然可以接著原本的進度繼續,也不會重新播放已經提醒過的內容。
完成這些功能後,推薦路線就不再只是地圖上的一條線。
除了能直接看出哪些推薦路段已經走過,也能知道下一個路口該往哪裡走、距離轉彎還有多遠。散步時就算不一直看手機,也能透過語音提醒跟著推薦路線前進。
另外把導航狀態保存到 SQLite 後,即使 App 中途重新啟動,也能接著原本的導航進度繼續。
到這裡 App 已經完成自由散步、推薦路線與基礎導航等核心功能。
接下來,就可以開始進入更進階的主題了!