Turn-by-turn navigation
Start a trip on a route, deliver position fixes, and receive the banner instruction, deviation status, phase, and speech-trigger channels. This tutorial rehearses the reference application’s guided-walk demo: the route you planned in Search and routing becomes a simulated drive with guidance. Every snippet compiles against the frozen FFI contract.
Build the controller and subscribe first
Subscribe to the topics before you start the trip, so the initial instruction dispatch of the trip start is received. The listener implements the four callbacks the contract carries:
import uniffi.map_native_core.MapNavigationController
import uniffi.map_native_core.NavigationStateEventListener
class GuidanceListener(
private val onInstruction: (NavigationInstruction) -> Unit,
private val onSpeech: (SpeechInstruction) -> Unit,
) : NavigationStateEventListener {
override fun onNavigationInstruction(instruction: NavigationInstruction) {
onInstruction(instruction)
}
override fun onSpeechInstruction(speechInstruction: SpeechInstruction) {
onSpeech(speechInstruction)
}
override fun onDeviationStatusChange(deviationStatusChange: DeviationStatusChange) {
// Update your reroute affordance here.
}
override fun onNavigationPhaseChange(navigationPhaseChange: NavigationPhaseChange) {
// Drive the navigation drawer's phase state here.
}
}
val navigationController = MapNavigationController()
val listener = GuidanceListener(onInstruction = ::showBanner, onSpeech = ::speak)
val subscriptions = listOf(
navigationController.subscribeToNavigationInstructions(listener),
navigationController.subscribeToSpeechInstructions(listener),
navigationController.subscribeToDeviationStatus(listener),
navigationController.subscribeToPhaseChanges(listener),
)
A subscription is cancelled with its handle when your screen leaves; dropping the reference does not unsubscribe.
Start the trip
Use the SDK’s default policies unless your product measured different ones:
import uniffi.map_native_core.defaultDeviationPolicy
import uniffi.map_native_core.defaultStepAdvancePolicy
import uniffi.map_native_core.UnitSystem
navigationController.startTrip(
route,
UnitSystem.METRIC,
defaultStepAdvancePolicy(),
defaultDeviationPolicy(),
)
The initial instruction payload dispatches before the call returns; the banner shows the first maneuver.
Deliver position fixes
One fix at a time, exactly like a live location source or a recorded trace replay would:
import uniffi.map_native_core.NavigationLocationUpdate
suspend fun replayFixes(
navigationController: MapNavigationController,
samples: Sequence<GeographicCoordinate>,
sampleIntervalMilliseconds: Long,
) {
for (sample in samples) {
navigationController.provideLocationUpdate(
NavigationLocationUpdate(
coordinate = sample,
horizontalAccuracyMeters = 8.0,
courseHeadingDegrees = null,
timestampSeconds = null,
),
)
delay(sampleIntervalMilliseconds)
}
}
Fixes without a reported accuracy never advance a step and are never judged for deviation, so a simulated source must carry its accuracy like a real fix does.
Voice the speech channel
The platform owns playback; the SDK owns when a line is newsworthy.
When onSpeechInstruction delivers a SpeechInstruction, hand its
verbatim text to the platform’s speech engine:
val textToSpeechEngine = TextToSpeech(context, this)
// onSpeechInstruction:
textToSpeechEngine.speak(
speechInstruction.utteranceSpeechText,
TextToSpeech.QUEUE_ADD,
null,
speechInstruction.utteranceId.toString(),
)
The utterance id is the contract’s monotonic id, so your dedup keeps its identity across repeats.
Reroute when guidance freezes
A deviating trip asks for a replacement route through the controller’s gates. Keep the state machine in your own coordinator, exactly as the reference application’s trip driver does:
if (tripState.deviation?.gradeKind == DeviationGradeKind.FULLY_OFF_ROUTE &&
navigationController.beginRerouteCalculation()
) {
coroutineScope.launch {
val replacement = mapSdk.calculateRoute(
listOf(currentFixCoordinate, originalDestination),
routeOptions,
)
navigationController.provideReroute(replacement, defaultStepAdvancePolicy())
}
} else {
navigationController.cancelRerouteCalculation()
}
Build the controller and subscribe first
The same four callbacks arrive as a Swift conforming object:
import RusticMapsSDK
final class GuidanceListener: NavigationStateEventListener {
func onNavigationInstruction(_ instruction: NavigationInstruction) {
// Update the banner instruction.
}
func onSpeechInstruction(_ speechInstruction: SpeechInstruction) {
// Hand the verbatim text to your speech engine.
}
func onDeviationStatusChange(_ deviationStatusChange: DeviationStatusChange) {
// Update the reroute affordance.
}
func onNavigationPhaseChange(_ navigationPhaseChange: NavigationPhaseChange) {
// Drive the phase state.
}
}
let navigationController = MapNavigationController()
let listener = GuidanceListener()
let subscription = navigationController.subscribeToNavigationInstructions(listener)
let speechSubscription = navigationController.subscribeToSpeechInstructions(listener)
Start the trip
try navigationController.startTrip(
route,
unitSystem: UnitSystem.metric,
stepAdvancePolicy: defaultStepAdvancePolicy(),
deviationPolicy: defaultDeviationPolicy()
)
Deliver position fixes
func deliver(_ fix: RecordedFix) {
navigationController.provideLocationUpdate(
NavigationLocationUpdate(
coordinate: fix.coordinate,
horizontalAccuracyMeters: fix.horizontalAccuracyMeters,
courseHeadingDegrees: fix.courseHeadingDegrees,
timestampSeconds: nil
)
)
if let tripState = navigationController.currentTripState() {
updateReadouts(tripState)
}
}
Voice the speech channel
let speechEngine = AVSpeechSynthesizer()
// In onSpeechInstruction:
let utterance = AVSpeechUtterance(string: speechInstruction.utteranceSpeechText)
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
speechEngine.speak(utterance)
Reroute when guidance freezes
if tripState.deviation?.gradeKind == .fullyOffRoute {
if navigationController.beginRerouteCalculation() {
let replacement = try await mapSdk.calculateRoute(
[currentFixCoordinate, originalDestination],
routeOptions: routeOptions
)
try navigationController.provideReroute(
replacement,
stepAdvancePolicy: defaultStepAdvancePolicy()
)
} else {
navigationController.cancelRerouteCalculation()
}
}
The trip state readback (currentTripState()) carries the phase, the
progress, and isCalculatingNewRoute so your UI can show the
recalculation honestly.
What the React Native surface carries
The TurboModule exposes the same contract members as promise-returning methods, including the navigation controller’s. A guided-walk integration builds a controller object, subscribes the topics, and delivers fixes:
import {
MapNavigationController,
defaultStepAdvancePolicy,
defaultDeviationPolicy,
} from '@angoratek/rustic-maps-sdk';
const navigationController = await new MapNavigationController();
const instructionsSubscription = await navigationController.subscribeToNavigationInstructions(listener);
const speechSubscription = await navigationController.subscribeToSpeechInstructions(listener);
The listener implements the four callback methods the contract carries
(onNavigationInstruction, onSpeechInstruction,
onDeviationStatusChange, onNavigationPhaseChange); each invocation
delivers the same payloads the native platforms receive, with lower
camel case field names.
Start the trip and deliver fixes
await navigationController.startTrip(
route,
'Metric',
defaultStepAdvancePolicy(),
defaultDeviationPolicy(),
);
// One fix per sample of your location source (or its recorded trace):
await navigationController.provideLocationUpdate({
coordinate: { latitude: sampleLatitude, longitude: sampleLongitude },
horizontalAccuracyMeters: 8.0,
courseHeadingDegrees: null,
timestampSeconds: null,
});
const tripState = await navigationController.currentTripState();
Voice the speech channel
The SDK triggers; your application voices. In the speech callback, hand
the verbatim text to the platform’s speech engine (Expo’s Speech, or
your own TTS wrapper), and use the delivery’s utteranceId for dedup:
function onSpeechInstruction(speechInstruction) {
Speech.speak(speechInstruction.utteranceSpeechText, {
language: 'en-US',
onDone: () => markUtteranceComplete(speechInstruction.utteranceId),
});
}
Scope note
The turn-by-turn controller surface is native-platform territory today: the WebAssembly seam ships the camera, rendering, and offline-routing slice, and no navigation controller rides it yet. The contract’s navigation surface stays native (the first map tutorial’s note covers the provider surface the same way).
What runs today
Turn-by-turn state is exercised end to end in the reference applications, and the browser harness runs the same renderer core the navigation follow-camera draws with: the live demo will include a guided walk once its bundle lands. Until then, run the navigation simulation on the native platforms; the demo loop page shows how the browser harness runs and what it exercises.