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)
}
}