Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.