diff --git a/README.md b/README.md index 586c4213..d53f2f74 100644 --- a/README.md +++ b/README.md @@ -721,7 +721,34 @@ 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 | + +Turn-by-turn events include navigation state, distance and time estimates, the current step, and +remaining steps. Step distances are in meters and durations are in seconds. `instruction`, +`exitNumber`, and `fullRoadName` can be absent; an arrival instruction can also be empty. +`roundaboutTurnNumber` is `-1` when the step is not in a roundabout. + +```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) { + return; + } + + if (step.maneuver === Maneuver.TURN_LEFT) { + console.log(step.instruction ?? 'Turn left'); + } + }); + + return () => setOnTurnByTurn(null); +}, [setOnTurnByTurn]); +``` ### MapViewAutoController (useNavigationAuto hook) diff --git a/example/src/screens/integration_tests/integration_test.ts b/example/src/screens/integration_tests/integration_test.ts index c8e953cf..202fde23 100644 --- a/example/src/screens/integration_tests/integration_test.ts +++ b/example/src/screens/integration_tests/integration_test.ts @@ -77,6 +77,46 @@ const NAVIGATOR_NOT_READY_ERROR_CODE = 'NO_NAVIGATOR_ERROR_CODE'; const NO_DESTINATIONS_ERROR_CODE = 'NO_DESTINATIONS'; export const NO_ERRORS_DETECTED_LABEL = 'No errors detected'; +const validateTurnByTurnPayload = ( + events: TurnByTurnEvent[] +): string | null => { + const event = events[0]; + if (!event) { + return 'Turn-by-turn event payload was empty'; + } + if ( + typeof event.navState !== 'number' || + typeof event.routeChanged !== 'boolean' + ) { + return 'Turn-by-turn event had invalid navigation state fields'; + } + if (!Array.isArray(event.getRemainingSteps)) { + return 'Turn-by-turn event had invalid remaining steps'; + } + + const step = event.currentStep ?? event.getRemainingSteps[0]; + if (!step) { + return 'Turn-by-turn event did not include a step'; + } + + const numericStepFields = [ + step.distanceFromPrevStepMeters, + step.timeFromPrevStepSeconds, + step.drivingSide, + step.stepNumber, + step.maneuver, + step.roundaboutTurnNumber, + ]; + if (!numericStepFields.every(field => typeof field === 'number')) { + return 'Turn-by-turn step had invalid numeric fields'; + } + if (step.instruction != null && typeof step.instruction !== 'string') { + return 'Turn-by-turn step had an invalid instruction'; + } + + return null; +}; + type NativeModuleError = { code?: string; }; @@ -1911,8 +1951,15 @@ export const testNavInfoEventsAfterCleanup = async (testTools: TestTools) => { let phase: 'first' | 'second' = 'first'; - setOnTurnByTurn(async (_events: TurnByTurnEvent[]) => { + setOnTurnByTurn(async (events: TurnByTurnEvent[]) => { if (phase === 'first') { + const payloadError = validateTurnByTurnPayload(events); + if (payloadError) { + setOnTurnByTurn(null); + failTest(payloadError); + return; + } + // Received navInfo in first session — now cleanup and re-init phase = 'second'; setOnTurnByTurn(null); diff --git a/ios/react-native-navigation-sdk/NavModule.mm b/ios/react-native-navigation-sdk/NavModule.mm index d8018575..a84f9078 100644 --- a/ios/react-native-navigation-sdk/NavModule.mm +++ b/ios/react-native-navigation-sdk/NavModule.mm @@ -1028,6 +1028,8 @@ - (NSDictionary *)getStepInfo:(GMSNavigationStepInfo *)stepInfo { [obj setValue:[NSNumber numberWithInteger:stepInfo.drivingSide] forKey:@"drivingSide"]; [obj setValue:[NSNumber numberWithInteger:stepInfo.stepNumber] forKey:@"stepNumber"]; [obj setValue:[NSNumber numberWithInteger:stepInfo.maneuver] forKey:@"maneuver"]; + [obj setValue:[NSNumber numberWithInteger:stepInfo.roundaboutTurnNumber] + forKey:@"roundaboutTurnNumber"]; [obj setValue:stepInfo.exitNumber forKey:@"exitNumber"]; [obj setValue:stepInfo.fullRoadName forKey:@"fullRoadName"]; [obj setValue:stepInfo.fullInstructionText forKey:@"instruction"]; diff --git a/src/native/NativeNavModule.ts b/src/native/NativeNavModule.ts index 65934b55..0f9da6a5 100644 --- a/src/native/NativeNavModule.ts +++ b/src/native/NativeNavModule.ts @@ -131,12 +131,18 @@ type TurnByTurnEventSpec = Readonly<{ getRemainingSteps: ReadonlyArray; }>; +// This mirrors the fields emitted by Android's StepInfo and iOS's +// GMSNavigationStepInfo. Keep it aligned with the public TurnByTurnStep type. type StepInfoSpec = Readonly<{ - instruction: string; - distanceMeters: Double; - durationSeconds: Double; - maneuver: string; - position: LatLngSpec; + instruction?: string | null; + distanceFromPrevStepMeters: Double; + timeFromPrevStepSeconds: Double; + drivingSide: Double; + stepNumber: Double; + maneuver: Double; + roundaboutTurnNumber: Double; + exitNumber?: string | null; + fullRoadName?: string | null; }>; enum RouteStatusSpec { diff --git a/src/navigation/navigation/types.ts b/src/navigation/navigation/types.ts index 55426509..142d685a 100644 --- a/src/navigation/navigation/types.ts +++ b/src/navigation/navigation/types.ts @@ -26,6 +26,9 @@ import type { TravelMode, Waypoint, TermsAndConditionsUIParams, + DrivingSide, + Maneuver, + NavState, } from '../types'; import { NavigationSessionStatus } from '../types'; @@ -550,8 +553,48 @@ export enum TaskRemovedBehavior { QUIT_SERVICE, } -/** - * Defines the turn-by-turn event data. - */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface TurnByTurnEvent {} +/** Information about one step in a turn-by-turn navigation route. */ +export interface TurnByTurnStep { + /** The step instruction. It can be absent or empty for arrival steps. */ + instruction?: string | null; + /** Distance from the previous step, in meters. */ + distanceFromPrevStepMeters: number; + /** Estimated duration of the step, in seconds. */ + timeFromPrevStepSeconds: number; + /** The side of the road used for this step. */ + drivingSide: DrivingSide; + /** The zero-based index of this step in the route. */ + stepNumber: number; + /** The maneuver to perform for this step. */ + maneuver: Maneuver; + /** The number of the exit after entering a roundabout, or -1 when not applicable. */ + roundaboutTurnNumber: number; + /** The route exit number, when available. */ + exitNumber?: string | null; + /** The full road name, when available. */ + fullRoadName?: string | null; +} + +/** Defines the turn-by-turn event data. */ +export interface TurnByTurnEvent { + /** The current navigation state. */ + navState: NavState; + /** Whether the route changed since the previous event. */ + routeChanged: boolean; + /** Distance to the current step, in meters. */ + distanceToCurrentStepMeters?: number; + /** Distance to the final destination, in meters. */ + distanceToFinalDestinationMeters?: number; + /** Estimated time to the current step, in seconds. */ + timeToCurrentStepSeconds?: number; + /** Distance to the next destination, in meters. */ + distanceToNextDestinationMeters?: number; + /** Estimated time to the next destination, in seconds. */ + timeToNextDestinationSeconds?: number; + /** Estimated time to the final destination, in seconds. */ + timeToFinalDestinationSeconds?: number; + /** The step currently being navigated, when available. */ + currentStep?: TurnByTurnStep; + /** The remaining route steps. This name mirrors the native event payload. */ + getRemainingSteps: TurnByTurnStep[]; +} diff --git a/src/navigation/types.ts b/src/navigation/types.ts index ec857040..e80410ca 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -43,6 +43,76 @@ export enum NavState { STOPPED = 3, } +/** The maneuver for a turn-by-turn navigation step. */ +export enum Maneuver { + UNKNOWN = 0, + DEPART = 1, + DESTINATION = 2, + DESTINATION_LEFT = 3, + DESTINATION_RIGHT = 4, + STRAIGHT = 5, + TURN_LEFT = 6, + TURN_RIGHT = 7, + TURN_KEEP_LEFT = 8, + TURN_KEEP_RIGHT = 9, + TURN_SLIGHT_LEFT = 10, + TURN_SLIGHT_RIGHT = 11, + TURN_SHARP_LEFT = 12, + TURN_SHARP_RIGHT = 13, + TURN_U_TURN_CLOCKWISE = 14, + TURN_U_TURN_COUNTERCLOCKWISE = 15, + MERGE_UNSPECIFIED = 16, + MERGE_LEFT = 17, + MERGE_RIGHT = 18, + FORK_LEFT = 19, + FORK_RIGHT = 20, + ON_RAMP_UNSPECIFIED = 21, + ON_RAMP_LEFT = 22, + ON_RAMP_RIGHT = 23, + ON_RAMP_KEEP_LEFT = 24, + ON_RAMP_KEEP_RIGHT = 25, + ON_RAMP_SLIGHT_LEFT = 26, + ON_RAMP_SLIGHT_RIGHT = 27, + ON_RAMP_SHARP_LEFT = 28, + ON_RAMP_SHARP_RIGHT = 29, + ON_RAMP_U_TURN_CLOCKWISE = 30, + ON_RAMP_U_TURN_COUNTERCLOCKWISE = 31, + OFF_RAMP_UNSPECIFIED = 32, + OFF_RAMP_LEFT = 33, + OFF_RAMP_RIGHT = 34, + OFF_RAMP_KEEP_LEFT = 35, + OFF_RAMP_KEEP_RIGHT = 36, + OFF_RAMP_SLIGHT_LEFT = 37, + OFF_RAMP_SLIGHT_RIGHT = 38, + OFF_RAMP_SHARP_LEFT = 39, + OFF_RAMP_SHARP_RIGHT = 40, + OFF_RAMP_U_TURN_CLOCKWISE = 41, + OFF_RAMP_U_TURN_COUNTERCLOCKWISE = 42, + ROUNDABOUT_CLOCKWISE = 43, + ROUNDABOUT_COUNTERCLOCKWISE = 44, + ROUNDABOUT_STRAIGHT_CLOCKWISE = 45, + ROUNDABOUT_STRAIGHT_COUNTERCLOCKWISE = 46, + ROUNDABOUT_LEFT_CLOCKWISE = 47, + ROUNDABOUT_LEFT_COUNTERCLOCKWISE = 48, + ROUNDABOUT_RIGHT_CLOCKWISE = 49, + ROUNDABOUT_RIGHT_COUNTERCLOCKWISE = 50, + ROUNDABOUT_SLIGHT_LEFT_CLOCKWISE = 51, + ROUNDABOUT_SLIGHT_LEFT_COUNTERCLOCKWISE = 52, + ROUNDABOUT_SLIGHT_RIGHT_CLOCKWISE = 53, + ROUNDABOUT_SLIGHT_RIGHT_COUNTERCLOCKWISE = 54, + ROUNDABOUT_SHARP_LEFT_CLOCKWISE = 55, + ROUNDABOUT_SHARP_LEFT_COUNTERCLOCKWISE = 56, + ROUNDABOUT_SHARP_RIGHT_CLOCKWISE = 57, + ROUNDABOUT_SHARP_RIGHT_COUNTERCLOCKWISE = 58, + ROUNDABOUT_U_TURN_CLOCKWISE = 59, + ROUNDABOUT_U_TURN_COUNTERCLOCKWISE = 60, + ROUNDABOUT_EXIT_CLOCKWISE = 61, + ROUNDABOUT_EXIT_COUNTERCLOCKWISE = 62, + FERRY_BOAT = 63, + FERRY_TRAIN = 64, + NAME_CHANGE = 65, +} + /** * Specify that the Navigation SDK should determine the appropriate day or night mode according to * user's location and local time.