Skip to content

Advanced integration with navigation#

It is possible to integrate directly with navigation sessions, for example to determine whether a navigation session is currently in progress, or even to build completely custom navigation UI.

Determining whether navigation is currently active#

The CitymapperNavigationTracking class allows listening to the RouteProgress of the currently active navigation session.

import CitymapperNavigation 

let nav = CitymapperNavigationTracking.shared

// Set a RouteProgressObserver to get callbacks
nav.addRouteProgressObserver(self) // assuming that `self` implements the RouteProgressObserver protocol

// To later unregister from updates, call
nav.removeRouteProgressObserver(self)
// Alternatively, the observer will automatically be removed when
// it deallocates
override func routeProgressUpdated(routeProgress: RouteProgress?) {
    // Handle the RouteProgress. A `nil` value indicates that no navigation session is active
}
val nav = CitymapperNavigationTracking.getInstance(context)

val disposable = nav.registerRouteProgressListener { routeProgress ->
    // Handle the RouteProgress. A `null` value indicates that no navigation session is active
}

// To later unregister from updates, for example in `onPause`, call
disposable.dispose()

When navigation tracking is active, the progress observer/listener will be called with the latest RouteProgress every time it updates. If no navigation session is active, it will be called with a nil/null value.

Tracking Configuration#

The TrackingConfiguration object passed to startNavigation allows customizing logging behavior during navigation.

enableRemoteLogging determines whether navigation logs are uploaded remotely, to help improve the quality of the navigation SDK. This is true by default.

enableOnDeviceLogging controls whether a navigation log will be written to a local file on the device. This is intended to help debugging during development. Only a single log is stored at any time, so if you begin navigation for a new route the previous log will be overwritten. This is false by default.

The local log file can be accessed by calling:

let fileUrl = CitymapperNavigationTracking.shared.currentNavigationLogFileUrl()
val logFile = CitymapperNavigationTracking.currentNavigationLogFile(context)

Rerouting#

Rerouting is handled automatically in this SDK. After navigation has started, if the user goes too far away from the existing path, an updated route will automatically be requested from Citymapper, and you will receive updated instructions and details via a routeProgressUpdated() delegate/listener callback.

Ending Navigation#

To stop any active navigation tracking, you can call endNavigation().

import CitymapperNavigation

CitymapperNavigationTracking.shared.endNavigation()
CitymapperNavigationTracking.getInstance(context).endNavigation()
CitymapperNavigationTracking.getInstance(context).endNavigation();

Reaching the End of a Route#

Once the user has arrived at the end of their Route, and a guidance event for "Arrive at your destination" has been triggered, no further guidance events will be triggered, and no further rerouting will occur.

However, the navigation state will remain active until you call endNavigation() (or call startNavigation() on a another Route).

You can determine if the navigation is in this state by checking to see if the isArrived property on the most recent RouteProgress received is true.

Providing Custom Voice Prompts#

When starting navigation using CitymapperDirectionsView, voice prompts are automatically provided when tapping the speech toggle.

If not using CitymapperDirectionsView register a GuidanceEventObserver/Listener to play speech in voice prompts for the user at appropriate times in their travel.

By default, the speech text will use measurement units determined from the device's locale settings, but this can be overridden to a particular set of units by passing a DistanceUnits value to createSpeechText().

The SDK currently supports the following localizations:

  • English
  • German
  • Spanish
  • French
  • Italian
  • Portuguese
  • Chinese (Simplified, Traditional, and Hong Kong variants)
  • Turkish
import CitymapperNavigation
import AVFoundation

let nav = CitymapperNavigationTracking.shared

// Set a GuidanceEventObserver to get callbacks
let guidanceEventObserver = // ... something implementing GuidanceEventObserver
nav.guidanceEventObserver = guidanceEventObserver

...

// inside the GuidanceEventObserver
func triggerGuidanceEvent(event: GuidanceEvent) {
    let instructionText = event.createSpeechText()

    // Play speech 
    // NOTE: For a more complete implementation see the demo app
    do {
        try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers])
        try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
        let utterance = AVSpeechUtterance(string: instructionText)
        let synth = AVSpeechSynthesizer()
        synth.speak(utterance)
    } catch {
        print(error)
    }
}
val nav = CitymapperNavigationTracking.getInstance(context)

// Set a GuidanceEventListener to get callbacks
val guidanceEventListener = // ... something implementing GuidanceEventListener
nav.registerGuidanceEventListener(guidanceEventListener)

val textToSpeech = TextToSpeech(context) { status ->

}

// inside the GuidanceEventListener
override fun onTriggerGuidanceEvent(guidanceEvent: GuidanceEvent) {
    // Play speech 
    // NOTE: For a more complete implementation, see the demo app
    val instructionText = guidanceEvent.createSpeechText(context)
    textToSpeech.speak(instructionText, TextToSpeech.QUEUE_ADD, Bundle.EMPTY, guidanceEvent.id)
}   
CitymapperNavigationTracking nav = CitymapperNavigationTracking.getInstance(context);

// Set a GuidanceEventListener to get callbacks
GuidanceEventListener guidanceEventListener = // ... something implementing GuidanceEventListener
nav.registerGuidanceEventListener(guidanceEventListener);

TextToSpeech textToSpeech = new TextToSpeech(context, status -> {

});

// inside the GuidanceEventListener
@Override
public void onTriggerGuidanceEvent(@NotNull GuidanceEvent guidanceEvent) {
    // Play speech 
    // NOTE: For a more complete implementation that handles audio more properly,
    // see the demo app
    String message = guidanceEvent.createSpeechText(context);
    textToSpeech.speak(message, TextToSpeech.QUEUE_ADD, Bundle.EMPTY, guidanceEvent.getId());
}

Building custom navigation UI#

Displaying the Next Instruction#

To know which instructions to display to the user as the next turn or instruction, inspect the RouteProgress object that's delivered to the RouteProgressDelegate/Listener that you've registered.

To directly access the instruction progress for the next instruction use .legProgress.nextInstructionProgress.

// inside your RouteProgressDelegate

func routeProgressUpdated(progress: RouteProgress) {
    guard let legProgress = progress.legProgress,
        instructionProgress = legProgress.instructionProgress else {
        return
    }
    let text = instructionProgress.instruction.descriptionText
    // update your display UI here
    print("In \(instructionProgress.distanceMetersUntilInstruction) meters, \(text)")
}
// inside your RouteProgressListener
override fun onRouteProgressUpdated(route: RouteProgress?) {
    val instructionProgress = route?.legProgress?.nextInstructionProgress ?: return
    val text = instructionProgress.instruction.descriptionText
    // update your display UI here
    Log.d("DEBUG", "In ${instructionProgress.distanceMetersUntilInstruction} meters, $text")
}
// inside your RouteProgressListener
@Override
public void onRouteProgressUpdated(@Nullable RouteProgress routeProgress) {
    LegProgress legProgress = routeProgress != null ? routeProgress.getLegProgress() : null;
    InstructionProgress instructionProgress = legProgress != null ? legProgress.getNextInstructionProgress() : null;
    if (instructionProgress == null) return;
    String text = instructionProgress.getInstruction().getDescriptionText();
    // update your display UI here
    Log.d("DEBUG", "In " + instructionProgress.getDistanceMetersUntilInstruction() + " meters, " + text);
}

Displaying a List of Upcoming Instructions#

Using the remainingInstructionsAfterNext property, the LegProgress object also provides a convenience access to the remaining instructions beyond the upcoming one in the current Leg.

To access only the next instruction, see Displaying the Next Instruction.

// inside your RouteProgressDelegate

func routeProgressUpdated(routeProgress: RouteProgress?) {
    // Update list of upcoming instructions in your display UI here
    let legProgress = routeProgress?.legProgress
    self.yourUIUpdateFunction(legProgress?.nextInstructionProgress, 
                            legProgress?.remainingInstructionsAfterNext)
}
// inside your RouteProgressListener

override fun onRouteProgressUpdated(routeProgress: RouteProgress?) {
    // Update list of upcoming instructions in your display UI here
    val legProgress = routeProgress?.legProgress
    yourUiUpdateFunction(
        legProgress?.nextInstructionProgress, 
        legProgress?.remainingInstructionsAfterNext
    )
}
// inside your RouteProgressListener

@Override
public void onRouteProgressUpdated(@Nullable RouteProgress routeProgress) {
    // Update list of upcoming instructions in your display UI here
    LegProgress legProgress = routeProgress != null ? routeProgress.getLegProgress() : null;
    if (legProgress == null) {
        yourUiUpdateFunction(null, null)
        return
    }
    yourUiUpdateFunction(legProgress.getNextInstructionProgress(),
                        legProgress.getRemainingInstructionsAfterNext())
}

Displaying the Route Path on a Map#

For turn-by-turn navigation, you'll generally want to show a line on the map indicating the path the user should follow. The exact styling is up to you, but you'll likely want to differentiate walking and vehicle (e.g. bike) parts of the path, and you'll probably want to draw the part of the line that the user has already passed with a different style than the road ahead.

The Citymapper SDK has some functionality to help you with this: in routeProgressUpdated callbacks, the progress.pathGeometrySegments gives you a path polyline broken down into a number of annotated segments. You can iterate through those and generate the appropriate lines in your mapping system of choice.

// inside your RouteProgressDelegate

func routeProgressUpdated(progress: RouteProgress) {
    // (remove existing path polylines from your map)

    for segment in progress.pathGeometrySegments {
        // (look at segment.travelMode and segment.pastOrFuture to determine
        // line styling)

        // (render segment.geometry with the styling determined above)
    }
}
// inside your RouteProgressListener

fun onRouteProgressUpdated(progress: RouteProgress) {
    // (remove existing path polylines from your map)

    for (segment in progress.pathGeometrySegments) {
        // (look at segment.travelMode and segment.pastOrFuture to determine
        // line styling)

        // (render segment.geometry with the styling determined above)
    }
}