Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,20 @@ Version 0.18.0 resolves the iOS Google Navigation SDK through Swift Package Mana
| Native SDKs | iOS Navigation SDK upgraded to `11.0.0` and resolved through Swift Package Manager |
| iOS display | `showTrafficLights` and `showStopSigns` no longer affect iOS navigation views |
| iOS maps | `MapType.TERRAIN` is unavailable during active navigation on iOS |
| Events | `TurnByTurnEvent` declares its payload fields; iOS emits it in every state |

### What changed

Google Navigation is now resolved through Swift Package Manager during `pod install`, rather than through a CocoaPods `GoogleNavigation` dependency. CocoaPods continues to manage React Native autolinking and other pods, but it no longer fetches Google Navigation. Configure framework linkage in your app's Podfile according to your app's dependency requirements; the example app uses dynamic frameworks.

Navigation SDK 11 always displays traffic lights and stop signs during navigation. The deprecated `showTrafficLights` and `showStopSigns` display options have no effect on iOS, but continue to apply on Android. Terrain map type is no longer available during active navigation on iOS; `MapType.TERRAIN` remains available for non-navigation maps. Add an `NSMotionUsageDescription` entry to your app's `Info.plist` to prevent future motion sensor-related crashes.

### Turn-by-turn events

`TurnByTurnEvent` declares the fields that `setOnTurnByTurn` delivers, and its steps are typed as `StepInfo` with a `Maneuver` value. Code that constructs `TurnByTurnEvent` objects, such as test mocks, must provide `navState`, `routeChanged`, and `getRemainingSteps`, and each step must provide `maneuver` and `drivingSide`.

On iOS, turn-by-turn events are emitted in every navigation state, matching Android. Events received while `navState` is not `NavState.ENROUTE` have no `currentStep`, so check `navState` or `currentStep` before reading step data.

### Reinstall iOS pods

Reinstall pods after upgrading:
Expand Down
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 the navigation state, distance and time estimates, the current step,
and remaining steps. Events are emitted in every navigation state, and `currentStep` is present only
while `navState` is `NavState.ENROUTE`. 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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,58 @@ 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);
}

// The SDK documents both null and -1 for steps that are not roundabouts, so either one leaves
// the key out.
Integer roundaboutTurnNumber = stepInfo.getRoundaboutTurnNumber();
if (roundaboutTurnNumber != null && roundaboutTurnNumber >= 0) {
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;
}

Expand Down
8 changes: 8 additions & 0 deletions example/e2e/navigation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
11 changes: 11 additions & 0 deletions example/src/screens/IntegrationTestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
testMinMaxZoomLevels,
testSetFollowingPerspective,
testNavInfoEventsAfterCleanup,
testTurnByTurnEventPayload,
NO_ERRORS_DETECTED_LABEL,
} from './integration_tests/integration_test';

Expand Down Expand Up @@ -327,6 +328,9 @@ const IntegrationTestsScreen = () => {
case 'testNavInfoEventsAfterCleanup':
await testNavInfoEventsAfterCleanup(getTestTools());
break;
case 'testTurnByTurnEventPayload':
await testTurnByTurnEventPayload(getTestTools());
break;
default:
resetTestState();
break;
Expand Down Expand Up @@ -563,6 +567,13 @@ const IntegrationTestsScreen = () => {
}}
testID="testNavInfoEventsAfterCleanup"
/>
<ExampleAppButton
title="testTurnByTurnEventPayload"
onPress={() => {
runTest('testTurnByTurnEventPayload');
}}
testID="testTurnByTurnEventPayload"
/>
</OverlayModal>
</View>
);
Expand Down
10 changes: 8 additions & 2 deletions example/src/screens/NavigationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
NavigationView,
MapColorScheme,
CameraPerspective,
Maneuver,
NavigationNightMode,
NavigationSessionStatus,
type ArrivalEvent,
Expand Down Expand Up @@ -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,
Expand Down
155 changes: 155 additions & 0 deletions example/src/screens/integration_tests/integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import {
AudioGuidanceMode,
CameraPerspective,
DrivingSide,
Maneuver,
NavState,
TravelMode,
NavigationSessionStatus,
RouteStatus,
Expand All @@ -27,6 +30,7 @@ import {
type MapViewController,
type NavigationController,
type NavigationViewController,
type StepInfo,
type TimeAndDistance,
type TurnByTurnEvent,
} from '@googlemaps/react-native-navigation-sdk';
Expand Down Expand Up @@ -1976,3 +1980,154 @@ export const testNavInfoEventsAfterCleanup = async (testTools: TestTools) => {

await initializeNavigation(navigationController, failTest);
};

const OPTIONAL_STEP_INTEGER_FIELDS = [
'stepNumber',
'distanceFromPrevStepMeters',
'timeFromPrevStepSeconds',
'roundaboutTurnNumber',
] as const;

const OPTIONAL_STEP_STRING_FIELDS = [
'instruction',
'fullRoadName',
'simpleRoadName',
'exitNumber',
] as const;

const OPTIONAL_EVENT_INTEGER_FIELDS = [
'distanceToCurrentStepMeters',
'timeToCurrentStepSeconds',
'distanceToNextDestinationMeters',
'timeToNextDestinationSeconds',
'distanceToFinalDestinationMeters',
'timeToFinalDestinationSeconds',
] as const;

/** Returns why a step does not match StepInfo, or undefined when it does. */
const findStepInfoMismatch = (
step: StepInfo,
label: string
): string | undefined => {
if (Maneuver[step.maneuver] === undefined) {
return `${label}.maneuver is not a Maneuver value: ${step.maneuver}`;
}
if (DrivingSide[step.drivingSide] === undefined) {
return `${label}.drivingSide is not a DrivingSide value: ${step.drivingSide}`;
}
for (const field of OPTIONAL_STEP_INTEGER_FIELDS) {
const value = step[field];
if (value !== undefined && !Number.isInteger(value)) {
return `${label}.${field} is not an integer: ${value}`;
}
}
for (const field of OPTIONAL_STEP_STRING_FIELDS) {
const value = step[field];
if (value !== undefined && typeof value !== 'string') {
return `${label}.${field} is not a string: ${value}`;
}
}
if (
step.roundaboutTurnNumber !== undefined &&
step.roundaboutTurnNumber < 0
) {
return `${label}.roundaboutTurnNumber must be omitted for non-roundabouts: ${step.roundaboutTurnNumber}`;
}
return undefined;
};

/**
* 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];
if (!event?.currentStep) {
return;
}

setOnTurnByTurn(null);
await navigationController.cleanup();

if (event.navState !== NavState.ENROUTE) {
return failTest(
`currentStep is set while navState is not ENROUTE: ${event.navState}`
);
}
if (typeof event.routeChanged !== 'boolean') {
return failTest(`routeChanged is not a boolean: ${event.routeChanged}`);
}
for (const field of OPTIONAL_EVENT_INTEGER_FIELDS) {
const value = event[field];
if (value !== undefined && !Number.isInteger(value)) {
return failTest(`${field} is not an integer: ${value}`);
}
}
if (!Array.isArray(event.getRemainingSteps)) {
return failTest('getRemainingSteps is not an array');
}

const mismatch =
findStepInfoMismatch(event.currentStep, 'currentStep') ??
event.getRemainingSteps
.map((step, index) =>
findStepInfoMismatch(step, `getRemainingSteps[${index}]`)
)
.find(message => message !== undefined);
if (mismatch) {
return failTest(mismatch);
}

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);
};
1 change: 0 additions & 1 deletion ios/react-native-navigation-sdk/INavigationCallback.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ NS_ASSUME_NONNULL_BEGIN
- (void)onRemainingTimeOrDistanceChanged;
- (void)onRouteChanged;
- (void)onArrival:(NSDictionary *)waypoint;
- (void)onTurnByTurn:(GMSNavigationNavInfo *)navInfo;
- (void)onTurnByTurn:(GMSNavigationNavInfo *)navInfo
distanceToNextDestinationMeters:(double)distanceToNextDestinationMeters
timeToNextDestinationSeconds:(double)timeToNextDestinationSeconds;
Expand Down
Loading
Loading