From 3da76192f0cc368d48f94e6933ee9d037b8a0ffc Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Fri, 18 Sep 2026 17:11:26 +0300 Subject: [PATCH 1/3] feat!: enhance turn-by-turn event payload and add tests for step fields --- README.md | 2 +- .../react/navsdk/ObjectTranslationUtil.java | 55 ++++++- example/e2e/navigation.test.js | 8 + .../src/screens/IntegrationTestsScreen.tsx | 11 ++ example/src/screens/NavigationScreen.tsx | 10 +- .../integration_tests/integration_test.ts | 140 ++++++++++++++++++ ios/react-native-navigation-sdk/NavModule.mm | 64 ++++---- src/native/NativeNavModule.ts | 15 +- src/navigation/navigation/types.ts | 94 +++++++++++- src/navigation/types.ts | 138 +++++++++++++++++ 10 files changed, 487 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 586c4213..b8da2e9b 100644 --- a/README.md +++ b/README.md @@ -721,7 +721,7 @@ setOnArrival(null); | `setOnReroutingRequestedByOffRoute` | `void` | Called when rerouting is triggered by off-route | | `setOnTrafficUpdated` | `void` | Called when traffic data is updated | | `setOnRemainingTimeOrDistanceChanged` | `void` | Called when remaining time or distance changes | -| `setOnTurnByTurn` | `{ navInfo: NavInfo }` | Called with turn-by-turn navigation info | +| `setOnTurnByTurn` | `TurnByTurnEvent[]` | Called with turn-by-turn navigation info | ### MapViewAutoController (useNavigationAuto hook) diff --git a/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java b/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java index bacef7f0..e1cc7ce3 100644 --- a/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java +++ b/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java @@ -156,17 +156,56 @@ public static WritableMap getMapFromWaypoint(Waypoint waypoint) { return map; } + /** + * Translates a step into the payload of the turn-by-turn event. Every getter used here except the + * maneuver and the driving side is nullable, and a value the Navigation SDK does not provide is + * left out of the payload so that it reads as undefined on the JavaScript side. + */ public static WritableMap getMapFromStepInfo(StepInfo stepInfo) { WritableMap map = Arguments.createMap(); - map.putInt("distanceFromPrevStepMeters", stepInfo.getDistanceFromPrevStepMeters()); - map.putInt("timeFromPrevStepSeconds", stepInfo.getTimeFromPrevStepSeconds()); - map.putInt("drivingSide", stepInfo.getDrivingSide()); - map.putInt("stepNumber", stepInfo.getStepNumber()); map.putInt("maneuver", stepInfo.getManeuver()); - map.putInt("roundaboutTurnNumber", stepInfo.getRoundaboutTurnNumber()); - map.putString("exitNumber", stepInfo.getExitNumber()); - map.putString("fullRoadName", stepInfo.getFullRoadName()); - map.putString("instruction", stepInfo.getFullInstructionText()); + map.putInt("drivingSide", stepInfo.getDrivingSide()); + + Integer distanceFromPrevStepMeters = stepInfo.getDistanceFromPrevStepMeters(); + if (distanceFromPrevStepMeters != null) { + map.putInt("distanceFromPrevStepMeters", distanceFromPrevStepMeters); + } + + Integer timeFromPrevStepSeconds = stepInfo.getTimeFromPrevStepSeconds(); + if (timeFromPrevStepSeconds != null) { + map.putInt("timeFromPrevStepSeconds", timeFromPrevStepSeconds); + } + + Integer stepNumber = stepInfo.getStepNumber(); + if (stepNumber != null) { + map.putInt("stepNumber", stepNumber); + } + + Integer roundaboutTurnNumber = stepInfo.getRoundaboutTurnNumber(); + if (roundaboutTurnNumber != null) { + map.putInt("roundaboutTurnNumber", roundaboutTurnNumber); + } + + String exitNumber = stepInfo.getExitNumber(); + if (exitNumber != null) { + map.putString("exitNumber", exitNumber); + } + + String fullRoadName = stepInfo.getFullRoadName(); + if (fullRoadName != null) { + map.putString("fullRoadName", fullRoadName); + } + + String simpleRoadName = stepInfo.getSimpleRoadName(); + if (simpleRoadName != null) { + map.putString("simpleRoadName", simpleRoadName); + } + + String instruction = stepInfo.getFullInstructionText(); + if (instruction != null) { + map.putString("instruction", instruction); + } + return map; } diff --git a/example/e2e/navigation.test.js b/example/e2e/navigation.test.js index bcf40526..cd91de75 100644 --- a/example/e2e/navigation.test.js +++ b/example/e2e/navigation.test.js @@ -106,4 +106,12 @@ describe('Navigation tests', () => { await expectNoErrors(); await expectSuccess(); }); + + it('NT11 - test turn-by-turn events carry the declared step fields', async () => { + await selectTestByName('testTurnByTurnEventPayload'); + await agreeToTermsAndConditions(); + await waitForTestToFinish(); + await expectNoErrors(); + await expectSuccess(); + }); }); diff --git a/example/src/screens/IntegrationTestsScreen.tsx b/example/src/screens/IntegrationTestsScreen.tsx index 292bd34e..84b1dc4e 100644 --- a/example/src/screens/IntegrationTestsScreen.tsx +++ b/example/src/screens/IntegrationTestsScreen.tsx @@ -58,6 +58,7 @@ import { testMinMaxZoomLevels, testSetFollowingPerspective, testNavInfoEventsAfterCleanup, + testTurnByTurnEventPayload, NO_ERRORS_DETECTED_LABEL, } from './integration_tests/integration_test'; @@ -327,6 +328,9 @@ const IntegrationTestsScreen = () => { case 'testNavInfoEventsAfterCleanup': await testNavInfoEventsAfterCleanup(getTestTools()); break; + case 'testTurnByTurnEventPayload': + await testTurnByTurnEventPayload(getTestTools()); + break; default: resetTestState(); break; @@ -563,6 +567,13 @@ const IntegrationTestsScreen = () => { }} testID="testNavInfoEventsAfterCleanup" /> + { + runTest('testTurnByTurnEventPayload'); + }} + testID="testTurnByTurnEventPayload" + /> ); diff --git a/example/src/screens/NavigationScreen.tsx b/example/src/screens/NavigationScreen.tsx index 83a82956..1c2f1e4c 100644 --- a/example/src/screens/NavigationScreen.tsx +++ b/example/src/screens/NavigationScreen.tsx @@ -23,6 +23,7 @@ import { NavigationView, MapColorScheme, CameraPerspective, + Maneuver, NavigationNightMode, NavigationSessionStatus, type ArrivalEvent, @@ -177,8 +178,13 @@ const NavigationScreen = () => { setOnTrafficUpdated(() => showSnackbar('Traffic Updated')); setOnStartGuidance(() => showSnackbar('Start Guidance')); - setOnTurnByTurn((_turnByTurn: TurnByTurnEvent[]) => { - console.debug(`Received ${_turnByTurn.length} turn-by-turn events`); + setOnTurnByTurn((turnByTurn: TurnByTurnEvent[]) => { + const currentStep = turnByTurn[0]?.currentStep; + console.debug( + `Received ${turnByTurn.length} turn-by-turn events, current step: ` + + `${currentStep ? Maneuver[currentStep.maneuver] : 'none'} ` + + `"${currentStep?.instruction ?? ''}"` + ); }); }, [ setOnNavigationReady, diff --git a/example/src/screens/integration_tests/integration_test.ts b/example/src/screens/integration_tests/integration_test.ts index c8e953cf..8bb6cb8c 100644 --- a/example/src/screens/integration_tests/integration_test.ts +++ b/example/src/screens/integration_tests/integration_test.ts @@ -17,6 +17,9 @@ import { AudioGuidanceMode, CameraPerspective, + DrivingSide, + Maneuver, + NavState, TravelMode, NavigationSessionStatus, RouteStatus, @@ -1976,3 +1979,140 @@ export const testNavInfoEventsAfterCleanup = async (testTools: TestTools) => { await initializeNavigation(navigationController, failTest); }; + +/** + * Verifies that a turn-by-turn event carries the fields that TurnByTurnEvent + * and StepInfo declare, so that the TypeScript types and the native emitters + * keep describing the same payload. + */ +export const testTurnByTurnEventPayload = async (testTools: TestTools) => { + const { + navigationController, + setOnNavigationReady, + setOnLocationChanged, + setOnTurnByTurn, + passTest, + failTest, + } = testTools; + + // Accept ToS first + if (!(await acceptToS(navigationController, failTest))) { + return; + } + + const startLocation: LatLng = { + lat: 37.79136614772824, + lng: -122.41565900473043, + }; + + setOnTurnByTurn(async (events: TurnByTurnEvent[]) => { + const event = events[0]; + const step = event?.currentStep; + if (!event || !step) { + return; + } + + setOnTurnByTurn(null); + await navigationController.cleanup(); + + if (NavState[event.navState] === undefined) { + return failTest(`navState is not a NavState value: ${event.navState}`); + } + if (typeof event.routeChanged !== 'boolean') { + return failTest(`routeChanged is not a boolean: ${event.routeChanged}`); + } + if (!Array.isArray(event.getRemainingSteps)) { + return failTest('getRemainingSteps is not an array'); + } + if (Maneuver[step.maneuver] === undefined) { + return failTest( + `currentStep.maneuver is not a Maneuver value: ${step.maneuver}` + ); + } + if (DrivingSide[step.drivingSide] === undefined) { + return failTest( + `currentStep.drivingSide is not a DrivingSide value: ${step.drivingSide}` + ); + } + if (typeof step.stepNumber !== 'number') { + return failTest( + `currentStep.stepNumber is not a number: ${step.stepNumber}` + ); + } + if (typeof step.distanceFromPrevStepMeters !== 'number') { + return failTest( + `currentStep.distanceFromPrevStepMeters is not a number: ${step.distanceFromPrevStepMeters}` + ); + } + if (typeof step.timeFromPrevStepSeconds !== 'number') { + return failTest( + `currentStep.timeFromPrevStepSeconds is not a number: ${step.timeFromPrevStepSeconds}` + ); + } + if (typeof step.instruction !== 'string') { + return failTest( + `currentStep.instruction is not a string: ${step.instruction}` + ); + } + if ( + step.fullRoadName !== undefined && + typeof step.fullRoadName !== 'string' + ) { + return failTest( + `currentStep.fullRoadName is not a string: ${step.fullRoadName}` + ); + } + if ( + step.simpleRoadName !== undefined && + typeof step.simpleRoadName !== 'string' + ) { + return failTest( + `currentStep.simpleRoadName is not a string: ${step.simpleRoadName}` + ); + } + if (step.exitNumber !== undefined && typeof step.exitNumber !== 'string') { + return failTest( + `currentStep.exitNumber is not a string: ${step.exitNumber}` + ); + } + if ( + step.roundaboutTurnNumber !== undefined && + typeof step.roundaboutTurnNumber !== 'number' + ) { + return failTest( + `currentStep.roundaboutTurnNumber is not a number: ${step.roundaboutTurnNumber}` + ); + } + + passTest(); + }); + + setOnNavigationReady(async () => { + disableVoiceGuidanceForTests(navigationController); + navigationController.setTurnByTurnLoggingEnabled(true); + + const located = await simulateAndWaitForLocation( + navigationController, + setOnLocationChanged, + startLocation + ); + if (!located) { + return failTest( + 'Timed out waiting for simulated location to be confirmed' + ); + } + await navigationController.setDestination({ + title: 'Grace Cathedral', + position: { + lat: 37.791957, + lng: -122.412529, + }, + }); + await navigationController.startGuidance(); + await navigationController.simulator.simulateLocationsAlongExistingRoute({ + speedMultiplier: 5, + }); + }); + + await initializeNavigation(navigationController, failTest); +}; diff --git a/ios/react-native-navigation-sdk/NavModule.mm b/ios/react-native-navigation-sdk/NavModule.mm index d8018575..968f65b5 100644 --- a/ios/react-native-navigation-sdk/NavModule.mm +++ b/ios/react-native-navigation-sdk/NavModule.mm @@ -31,6 +31,20 @@ static NSString *const kNoDestinationsErrorMessage = @"Destinations not set"; static dispatch_once_t deprecatedDisplayOptionsWarningOnce; +/** + * Adds a Navigation SDK distance or duration to a turn-by-turn payload, rounded to the whole + * meters and seconds that the JavaScript side declares. The navigator reports + * `CLLocationDistanceMax` and `CLTimeIntervalMax` while no route is available, and rounding a + * value that does not fit an integer is undefined, so an unavailable value is left out of the + * payload rather than converted. + */ +static void setRoundedValue(NSMutableDictionary *payload, NSString *key, double value) { + if (!isfinite(value) || fabs(value) >= (double)NSIntegerMax) { + return; + } + [payload setValue:@(llround(value)) forKey:key]; +} + @implementation NavModule { GMSNavigationSession *_session; NSMutableArray *_destinations; @@ -967,34 +981,13 @@ - (void)onTurnByTurn:(GMSNavigationNavInfo *)navInfo [obj setValue:[NSNumber numberWithLong:navInfo.navState] forKey:@"navState"]; [obj setValue:[NSNumber numberWithBool:navInfo.routeChanged] forKey:@"routeChanged"]; - if (navInfo.distanceToCurrentStepMeters) { - [obj setValue:[NSNumber numberWithLong:navInfo.distanceToCurrentStepMeters] - forKey:@"distanceToCurrentStepMeters"]; - } - - if (navInfo.distanceToFinalDestinationMeters) { - [obj setValue:[NSNumber numberWithLong:navInfo.distanceToFinalDestinationMeters] - forKey:@"distanceToFinalDestinationMeters"]; - } - if (navInfo.timeToCurrentStepSeconds) { - [obj setValue:[NSNumber numberWithLong:navInfo.timeToCurrentStepSeconds] - forKey:@"timeToCurrentStepSeconds"]; - } - - if (distanceToNextDestinationMeters) { - [obj setValue:[NSNumber numberWithLong:distanceToNextDestinationMeters] - forKey:@"distanceToNextDestinationMeters"]; - } - - if (timeToNextDestinationSeconds) { - [obj setValue:[NSNumber numberWithLong:timeToNextDestinationSeconds] - forKey:@"timeToNextDestinationSeconds"]; - } - - if (navInfo.timeToFinalDestinationSeconds) { - [obj setValue:[NSNumber numberWithLong:navInfo.timeToFinalDestinationSeconds] - forKey:@"timeToFinalDestinationSeconds"]; - } + setRoundedValue(obj, @"distanceToCurrentStepMeters", navInfo.distanceToCurrentStepMeters); + setRoundedValue(obj, @"distanceToFinalDestinationMeters", + navInfo.distanceToFinalDestinationMeters); + setRoundedValue(obj, @"timeToCurrentStepSeconds", navInfo.timeToCurrentStepSeconds); + setRoundedValue(obj, @"timeToFinalDestinationSeconds", navInfo.timeToFinalDestinationSeconds); + setRoundedValue(obj, @"distanceToNextDestinationMeters", distanceToNextDestinationMeters); + setRoundedValue(obj, @"timeToNextDestinationSeconds", timeToNextDestinationSeconds); if (navInfo.currentStep != NULL) { [obj setObject:[self getStepInfo:navInfo.currentStep] forKey:@"currentStep"]; @@ -1021,15 +1014,22 @@ - (void)onTurnByTurn:(GMSNavigationNavInfo *)navInfo - (NSDictionary *)getStepInfo:(GMSNavigationStepInfo *)stepInfo { NSMutableDictionary *obj = [[NSMutableDictionary alloc] init]; - [obj setValue:[NSNumber numberWithInteger:stepInfo.distanceFromPrevStepMeters] - forKey:@"distanceFromPrevStepMeters"]; - [obj setValue:[NSNumber numberWithInteger:stepInfo.timeFromPrevStepSeconds] - forKey:@"timeFromPrevStepSeconds"]; + setRoundedValue(obj, @"distanceFromPrevStepMeters", stepInfo.distanceFromPrevStepMeters); + setRoundedValue(obj, @"timeFromPrevStepSeconds", stepInfo.timeFromPrevStepSeconds); [obj setValue:[NSNumber numberWithInteger:stepInfo.drivingSide] forKey:@"drivingSide"]; [obj setValue:[NSNumber numberWithInteger:stepInfo.stepNumber] forKey:@"stepNumber"]; [obj setValue:[NSNumber numberWithInteger:stepInfo.maneuver] forKey:@"maneuver"]; + + // Only roundabout steps count an exit; the SDK reports -1 for every other step, and the + // JavaScript side expects the key to be absent there. + if (stepInfo.roundaboutTurnNumber >= 0) { + [obj setValue:[NSNumber numberWithInteger:stepInfo.roundaboutTurnNumber] + forKey:@"roundaboutTurnNumber"]; + } + [obj setValue:stepInfo.exitNumber forKey:@"exitNumber"]; [obj setValue:stepInfo.fullRoadName forKey:@"fullRoadName"]; + [obj setValue:stepInfo.simpleRoadName forKey:@"simpleRoadName"]; [obj setValue:stepInfo.fullInstructionText forKey:@"instruction"]; return obj; diff --git a/src/native/NativeNavModule.ts b/src/native/NativeNavModule.ts index 65934b55..f2d0e766 100644 --- a/src/native/NativeNavModule.ts +++ b/src/native/NativeNavModule.ts @@ -132,11 +132,16 @@ type TurnByTurnEventSpec = Readonly<{ }>; type StepInfoSpec = Readonly<{ - instruction: string; - distanceMeters: Double; - durationSeconds: Double; - maneuver: string; - position: LatLngSpec; + instruction?: string; + fullRoadName?: string; + simpleRoadName?: string; + maneuver: Double; + drivingSide: Double; + stepNumber?: Double; + distanceFromPrevStepMeters?: Double; + timeFromPrevStepSeconds?: Double; + exitNumber?: string; + roundaboutTurnNumber?: Double; }>; enum RouteStatusSpec { diff --git a/src/navigation/navigation/types.ts b/src/navigation/navigation/types.ts index 55426509..ccdb4d2c 100644 --- a/src/navigation/navigation/types.ts +++ b/src/navigation/navigation/types.ts @@ -19,6 +19,9 @@ import type { AlternateRoutingStrategy, AudioGuidance, AudioGuidanceSettings, + DrivingSide, + Maneuver, + NavState, RouteSegment, RouteStatus, RoutingStrategy, @@ -550,8 +553,95 @@ export enum TaskRemovedBehavior { QUIT_SERVICE, } +/** + * Information about a single step along the navigation route. + * + * Fields that the Navigation SDK does not provide for a step are omitted from + * the payload, so every optional field has to be checked before use. + */ +export interface StepInfo { + /** + * The full text of the instruction for this step, for example + * "Turn left onto Lean Ave." + * + * The arrival step is emitted with an empty instruction, so treat an empty + * string the same way as a missing one. + */ + instruction?: string; + + /** + * The full road name for this step, containing all information for the road + * including direction, for example "Exit 9A / North First Street". + */ + fullRoadName?: string; + + /** + * The shortened version of the road name, which may leave out identifying + * information such as direction or the name of an exit, for example + * "Exit 9A". + */ + simpleRoadName?: string; + + /** The navigation action to take for this step. */ + maneuver: Maneuver; + + /** Whether this step is on a drive-on-right or drive-on-left route. */ + drivingSide: DrivingSide; + + /** The index of the step in the list of all steps in the route, starting at 0. */ + stepNumber?: number; + + /** The total distance of this step. This value does not change. */ + distanceFromPrevStepMeters?: number; + + /** The estimated time it takes to complete this step. This value does not change. */ + timeFromPrevStepSeconds?: number; + + /** Exit number of this step, for steps that leave a turnpike or freeway. */ + exitNumber?: string; + + /** + * The counted number of the exit to take relative to the location where the + * roundabout was entered. Only set for roundabouts. + */ + roundaboutTurnNumber?: number; +} + /** * Defines the turn-by-turn event data. */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface TurnByTurnEvent {} +export interface TurnByTurnEvent { + /** The current state of navigation. */ + navState: NavState; + + /** + * Whether the route has changed since the previous event. A route change may + * be caused by a reroute, the addition or removal of a waypoint, the user + * selecting or driving onto an alternate route, or a traffic update. + */ + routeChanged: boolean; + + /** The estimated remaining distance along the route to the current step. */ + distanceToCurrentStepMeters?: number; + + /** The estimated remaining time along the route to the current step. */ + timeToCurrentStepSeconds?: number; + + /** The estimated remaining distance to the next destination. */ + distanceToNextDestinationMeters?: number; + + /** The estimated remaining time to the next destination. */ + timeToNextDestinationSeconds?: number; + + /** The estimated remaining distance to the final destination. */ + distanceToFinalDestinationMeters?: number; + + /** The estimated remaining time to the final destination. */ + timeToFinalDestinationSeconds?: number; + + /** Information about the upcoming maneuver step. Only set while navState is {@link NavState.ENROUTE}. */ + currentStep?: StepInfo; + + /** The remaining steps after the current step. */ + getRemainingSteps: StepInfo[]; +} diff --git a/src/navigation/types.ts b/src/navigation/types.ts index ec857040..15cc3bc0 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -29,6 +29,144 @@ export enum DrivingSide { RIGHT = 2, } +/** + * A set of values that specify the navigation action to take. + */ +export enum Maneuver { + /** Unknown maneuver. */ + UNKNOWN = 0, + /** Starting point of the maneuver. */ + DEPART = 1, + /** Arrival at a destination. */ + DESTINATION = 2, + /** Arrival at a destination located on the left side of the road. */ + DESTINATION_LEFT = 3, + /** Arrival at a destination located on the right side of the road. */ + DESTINATION_RIGHT = 4, + /** Continue straight. */ + STRAIGHT = 5, + /** Regular left turn at an intersection. */ + TURN_LEFT = 6, + /** Regular right turn at an intersection. */ + TURN_RIGHT = 7, + /** Keep left as the road diverges. */ + TURN_KEEP_LEFT = 8, + /** Keep right as the road diverges. */ + TURN_KEEP_RIGHT = 9, + /** Slight left turn at an intersection. */ + TURN_SLIGHT_LEFT = 10, + /** Slight right turn at an intersection. */ + TURN_SLIGHT_RIGHT = 11, + /** Sharp left turn at an intersection. */ + TURN_SHARP_LEFT = 12, + /** Sharp right turn at an intersection. */ + TURN_SHARP_RIGHT = 13, + /** Clockwise turn onto the opposite side of the street. */ + TURN_U_TURN_CLOCKWISE = 14, + /** Counterclockwise turn onto the opposite side of the street. */ + TURN_U_TURN_COUNTERCLOCKWISE = 15, + /** Current road joins another. */ + MERGE_UNSPECIFIED = 16, + /** Current road joins another on the left. */ + MERGE_LEFT = 17, + /** Current road joins another on the right. */ + MERGE_RIGHT = 18, + /** Current road joins another road slightly on the left. */ + FORK_LEFT = 19, + /** Current road joins another road slightly on the right. */ + FORK_RIGHT = 20, + /** Enter a turnpike or freeway. */ + ON_RAMP_UNSPECIFIED = 21, + /** Regular left turn to enter a turnpike or freeway. */ + ON_RAMP_LEFT = 22, + /** Regular right turn to enter a turnpike or freeway. */ + ON_RAMP_RIGHT = 23, + /** Keep to the left side of the road when entering a turnpike or freeway as the road diverges. */ + ON_RAMP_KEEP_LEFT = 24, + /** Keep to the right side of the road when entering a turnpike or freeway as the road diverges. */ + ON_RAMP_KEEP_RIGHT = 25, + /** Slight left turn to enter a turnpike or freeway. */ + ON_RAMP_SLIGHT_LEFT = 26, + /** Slight right turn to enter a turnpike or freeway. */ + ON_RAMP_SLIGHT_RIGHT = 27, + /** Sharp left turn to enter a turnpike or freeway. */ + ON_RAMP_SHARP_LEFT = 28, + /** Sharp right turn to enter a turnpike or freeway. */ + ON_RAMP_SHARP_RIGHT = 29, + /** Clockwise turn onto the opposite side of the street to enter a turnpike or freeway. */ + ON_RAMP_U_TURN_CLOCKWISE = 30, + /** Counterclockwise turn onto the opposite side of the street to enter a turnpike or freeway. */ + ON_RAMP_U_TURN_COUNTERCLOCKWISE = 31, + /** Exit a turnpike or freeway. */ + OFF_RAMP_UNSPECIFIED = 32, + /** Regular left turn to exit a turnpike or freeway. */ + OFF_RAMP_LEFT = 33, + /** Regular right turn to exit a turnpike or freeway. */ + OFF_RAMP_RIGHT = 34, + /** Keep to the left side of the road when exiting a turnpike or freeway as the road diverges. */ + OFF_RAMP_KEEP_LEFT = 35, + /** Keep to the right side of the road when exiting a turnpike or freeway as the road diverges. */ + OFF_RAMP_KEEP_RIGHT = 36, + /** Slight left turn to exit a turnpike or freeway. */ + OFF_RAMP_SLIGHT_LEFT = 37, + /** Slight right turn to exit a turnpike or freeway. */ + OFF_RAMP_SLIGHT_RIGHT = 38, + /** Sharp left turn to exit a turnpike or freeway. */ + OFF_RAMP_SHARP_LEFT = 39, + /** Sharp right turn to exit a turnpike or freeway. */ + OFF_RAMP_SHARP_RIGHT = 40, + /** Clockwise turn onto the opposite side of the street to exit a turnpike or freeway. */ + OFF_RAMP_U_TURN_CLOCKWISE = 41, + /** Counterclockwise turn onto the opposite side of the street to exit a turnpike or freeway. */ + OFF_RAMP_U_TURN_COUNTERCLOCKWISE = 42, + /** Enter a roundabout in the clockwise direction. */ + ROUNDABOUT_CLOCKWISE = 43, + /** Enter a roundabout in the counterclockwise direction. */ + ROUNDABOUT_COUNTERCLOCKWISE = 44, + /** Enter a roundabout in the clockwise direction and continue straight. */ + ROUNDABOUT_STRAIGHT_CLOCKWISE = 45, + /** Enter a roundabout in the counterclockwise direction and continue straight. */ + ROUNDABOUT_STRAIGHT_COUNTERCLOCKWISE = 46, + /** Enter a roundabout in the clockwise direction and turn left. */ + ROUNDABOUT_LEFT_CLOCKWISE = 47, + /** Enter a roundabout in the counterclockwise direction and turn left. */ + ROUNDABOUT_LEFT_COUNTERCLOCKWISE = 48, + /** Enter a roundabout in the clockwise direction and turn right. */ + ROUNDABOUT_RIGHT_CLOCKWISE = 49, + /** Enter a roundabout in the counterclockwise direction and turn right. */ + ROUNDABOUT_RIGHT_COUNTERCLOCKWISE = 50, + /** Enter a roundabout in the clockwise direction and turn slightly left. */ + ROUNDABOUT_SLIGHT_LEFT_CLOCKWISE = 51, + /** Enter a roundabout in the counterclockwise direction and turn slightly to the left. */ + ROUNDABOUT_SLIGHT_LEFT_COUNTERCLOCKWISE = 52, + /** Enter a roundabout in the clockwise direction and turn slightly to the right. */ + ROUNDABOUT_SLIGHT_RIGHT_CLOCKWISE = 53, + /** Enter a roundabout in the counterclockwise direction and turn slightly to the right. */ + ROUNDABOUT_SLIGHT_RIGHT_COUNTERCLOCKWISE = 54, + /** Enter a roundabout in the clockwise direction and turn sharply to the left. */ + ROUNDABOUT_SHARP_LEFT_CLOCKWISE = 55, + /** Enter a roundabout in the counterclockwise direction and turn sharply to the left. */ + ROUNDABOUT_SHARP_LEFT_COUNTERCLOCKWISE = 56, + /** Enter a roundabout in the clockwise direction and turn sharply to the right. */ + ROUNDABOUT_SHARP_RIGHT_CLOCKWISE = 57, + /** Enter a roundabout in the counterclockwise direction and turn sharply to the right. */ + ROUNDABOUT_SHARP_RIGHT_COUNTERCLOCKWISE = 58, + /** Enter a roundabout in the clockwise direction and turn onto the opposite side of the street. */ + ROUNDABOUT_U_TURN_CLOCKWISE = 59, + /** Enter a roundabout in the counterclockwise direction and turn onto the opposite side of the street. */ + ROUNDABOUT_U_TURN_COUNTERCLOCKWISE = 60, + /** Exit a roundabout in the clockwise direction. */ + ROUNDABOUT_EXIT_CLOCKWISE = 61, + /** Exit a roundabout in the counterclockwise direction. */ + ROUNDABOUT_EXIT_COUNTERCLOCKWISE = 62, + /** Take the boat ferry. */ + FERRY_BOAT = 63, + /** Take the train ferry. */ + FERRY_TRAIN = 64, + /** The street name changes. */ + NAME_CHANGE = 65, +} + /** * The state of navigation in Navigation SDK. */ From a1b793f0d4bd592b871a76e298f186fca9b0a4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Tue, 22 Sep 2026 11:27:10 +0800 Subject: [PATCH 2/3] docs: improve turn by turn documentation --- README.md | 26 ++++++++++++++++++++++++++ src/native/NativeNavModule.ts | 2 ++ 2 files changed, 28 insertions(+) diff --git a/README.md b/README.md index b8da2e9b..cc2ae21f 100644 --- a/README.md +++ b/README.md @@ -723,6 +723,32 @@ setOnArrival(null); | `setOnRemainingTimeOrDistanceChanged` | `void` | Called when remaining time or distance changes | | `setOnTurnByTurn` | `TurnByTurnEvent[]` | Called with turn-by-turn navigation info | +Turn-by-turn events include the navigation state, distance and time estimates, the current step, +and remaining steps. Distances are in meters and durations are in seconds. Step fields that the +Navigation SDK does not provide are omitted. In particular, `roundaboutTurnNumber` is present only +for roundabout steps; check that optional fields are defined before using them. + +```tsx +import { useEffect } from 'react'; +import { + Maneuver, + useNavigation, +} from '@googlemaps/react-native-navigation-sdk'; + +const { setOnTurnByTurn } = useNavigation(); + +useEffect(() => { + setOnTurnByTurn(events => { + const step = events[0]?.currentStep; + if (step?.maneuver === Maneuver.TURN_LEFT) { + console.log(step.instruction ?? 'Turn left'); + } + }); + + return () => setOnTurnByTurn(null); +}, [setOnTurnByTurn]); +``` + ### MapViewAutoController (useNavigationAuto hook) For Android Auto and CarPlay support, the `useNavigationAuto()` hook provides a `MapViewAutoController` and auto-specific listener setters: diff --git a/src/native/NativeNavModule.ts b/src/native/NativeNavModule.ts index f2d0e766..47dabe92 100644 --- a/src/native/NativeNavModule.ts +++ b/src/native/NativeNavModule.ts @@ -131,6 +131,8 @@ type TurnByTurnEventSpec = Readonly<{ getRemainingSteps: ReadonlyArray; }>; +// This mirrors the fields emitted by Android's StepInfo and iOS's +// GMSNavigationStepInfo. Keep it aligned with the public StepInfo type. type StepInfoSpec = Readonly<{ instruction?: string; fullRoadName?: string; From 4e989e0037f30e54332074bc1b180edc727df9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Tue, 22 Sep 2026 11:28:09 +0800 Subject: [PATCH 3/3] fix: omit non-roundabout turn numbers on android --- .../android/react/navsdk/ObjectTranslationUtil.java | 2 +- example/src/screens/integration_tests/integration_test.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java b/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java index e1cc7ce3..13d35ca4 100644 --- a/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java +++ b/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java @@ -182,7 +182,7 @@ public static WritableMap getMapFromStepInfo(StepInfo stepInfo) { } Integer roundaboutTurnNumber = stepInfo.getRoundaboutTurnNumber(); - if (roundaboutTurnNumber != null) { + if (roundaboutTurnNumber != null && roundaboutTurnNumber >= 0) { map.putInt("roundaboutTurnNumber", roundaboutTurnNumber); } diff --git a/example/src/screens/integration_tests/integration_test.ts b/example/src/screens/integration_tests/integration_test.ts index 8bb6cb8c..38271840 100644 --- a/example/src/screens/integration_tests/integration_test.ts +++ b/example/src/screens/integration_tests/integration_test.ts @@ -2083,6 +2083,14 @@ export const testTurnByTurnEventPayload = async (testTools: TestTools) => { `currentStep.roundaboutTurnNumber is not a number: ${step.roundaboutTurnNumber}` ); } + if ( + step.roundaboutTurnNumber !== undefined && + step.roundaboutTurnNumber < 0 + ) { + return failTest( + `currentStep.roundaboutTurnNumber must be omitted for non-roundabouts: ${step.roundaboutTurnNumber}` + ); + } passTest(); });