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
184 changes: 43 additions & 141 deletions example/src/helpers/routesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,169 +14,71 @@
* limitations under the License.
*/

// Note: This Routes API implementation is meant to be used only to
// support the example app, and only includes the bare minimum to get
// the route tokens.
// Minimal Routes API implementation for fetching route tokens.
// Route tokens are currently only supported for DRIVE travel mode.
// See: https://developers.google.com/maps/documentation/routes/route_token

import type { LatLng, Waypoint } from '@googlemaps/react-native-navigation-sdk';
import type { LatLng } from '@googlemaps/react-native-navigation-sdk';

const ROUTES_API_URL = 'https://routes.googleapis.com';
const COMPUTE_ROUTES_URL = `${ROUTES_API_URL}/directions/v2:computeRoutes`;
const COMPUTE_ROUTES_URL =
'https://routes.googleapis.com/directions/v2:computeRoutes';

/**
* Travel modes supported by the Routes API.
*/
export type RoutesApiTravelMode = 'DRIVE' | 'BICYCLE' | 'WALK' | 'TWO_WHEELER';

/**
* Routing preference options for the Routes API.
*/
export type RoutingPreference = 'TRAFFIC_AWARE' | 'TRAFFIC_AWARE_OPTIMAL';

/**
* Options for the Routes API request.
*/
export interface RoutesApiOptions {
/** The travel mode for the route. Defaults to 'DRIVE'. */
travelMode?: RoutesApiTravelMode;
/** The routing preference. Defaults to 'TRAFFIC_AWARE'. */
routingPreference?: RoutingPreference;
}

/**
* Response from the Routes API containing route tokens.
*/
export interface RoutesApiResponse {
/** List of route tokens returned by the API. */
routeTokens: string[];
}

/**
* Converts a Waypoint to the Routes API waypoint format.
*/
function toRoutesApiWaypoint(
waypoint: Waypoint | LatLng,
via: boolean = false
): Record<string, unknown> {
const output: Record<string, unknown> = { via };

// Check if it's a Waypoint with placeId
if ('placeId' in waypoint && waypoint.placeId) {
output.placeId = waypoint.placeId;
} else {
// Handle LatLng or Waypoint with position
let lat: number;
let lng: number;

if ('position' in waypoint && waypoint.position) {
lat = waypoint.position.lat;
lng = waypoint.position.lng;
} else if ('lat' in waypoint && 'lng' in waypoint) {
lat = waypoint.lat;
lng = waypoint.lng;
} else {
throw new Error(
'Invalid waypoint: Either position or placeId must be provided.'
);
}

const location: Record<string, unknown> = {
latLng: {
latitude: lat,
longitude: lng,
},
};

// Add preferred heading if available
if ('preferredHeading' in waypoint && waypoint.preferredHeading != null) {
location.heading = waypoint.preferredHeading;
}

output.location = location;
}

return output;
}

/**
* Queries the Google Maps Routes API and returns a list of route tokens.
* Fetches a route token from the Google Maps Routes API.
* Route tokens are only supported for DRIVE travel mode.
*
* @param apiKey - The Google Maps API key with Routes API enabled.
* @param waypoints - A list of waypoints representing the route (minimum 2: origin and destination).
* @param options - Optional configuration for the route request.
* @returns A promise that resolves to a list of route tokens.
* @throws Error if the request fails or returns no route tokens.
* @param apiKey - Google Maps API key with Routes API enabled.
* @param origin - Starting location.
* @param destination - Ending location.
* @returns The route token string.
*
* @example
* ```typescript
* const tokens = await getRouteToken(
* 'YOUR_API_KEY',
* [
* { lat: 37.7749, lng: -122.4194 }, // Origin
* { lat: 37.3382, lng: -121.8863 }, // Destination
* ],
* { travelMode: 'DRIVE' }
* );
* ```
* @see https://developers.google.com/maps/documentation/routes/route_token
*/
export async function getRouteToken(
apiKey: string,
waypoints: (Waypoint | LatLng)[],
options: RoutesApiOptions = {}
): Promise<string[]> {
origin: LatLng,
destination: LatLng
): Promise<string> {
if (!apiKey || apiKey.trim() === '') {
throw new Error(
'API key is required. Please provide a valid Google Maps API key.'
);
}

if (waypoints.length < 2) {
throw new Error(
'At least two waypoints (origin and destination) are required.'
);
throw new Error('API key is required.');
}

const { travelMode = 'DRIVE', routingPreference = 'TRAFFIC_AWARE' } = options;

const origin = waypoints[0]!;
const destination = waypoints[waypoints.length - 1]!;
const intermediates = waypoints.slice(1, -1);

const requestBody: Record<string, unknown> = {
origin: toRoutesApiWaypoint(origin),
destination: toRoutesApiWaypoint(destination),
intermediates: intermediates.map(wp => toRoutesApiWaypoint(wp, true)),
travelMode,
routingPreference,
};

const headers: Record<string, string> = {
'X-Goog-Api-Key': apiKey,
'X-Goog-FieldMask': 'routes.routeToken',
'Content-Type': 'application/json',
};

const response = await fetch(COMPUTE_ROUTES_URL, {
method: 'POST',
headers,
body: JSON.stringify(requestBody),
headers: {
'X-Goog-Api-Key': apiKey,
'X-Goog-FieldMask': 'routes.routeToken',
'Content-Type': 'application/json',
},
body: JSON.stringify({
origin: {
location: {
latLng: { latitude: origin.lat, longitude: origin.lng },
},
},
destination: {
location: {
latLng: { latitude: destination.lat, longitude: destination.lng },
},
},
travelMode: 'DRIVE',
routingPreference: 'TRAFFIC_AWARE',
}),
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to get route tokens: ${response.statusText}\n${errorText}`
);
throw new Error(`Routes API error: ${response.statusText}\n${errorText}`);
}

const responseData = (await response.json()) as {
routes?: { routeToken: string }[];
const data = (await response.json()) as {
routes?: { routeToken?: string }[];
};
const routes = responseData.routes;

if (!routes || routes.length === 0) {
throw new Error('No routes returned from the Routes API.');
const routeToken = data.routes?.[0]?.routeToken;
if (!routeToken) {
throw new Error('No route token returned from the Routes API.');
}

return routes.map(route => route.routeToken);
return routeToken;
}
96 changes: 10 additions & 86 deletions example/src/screens/RouteTokenScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ import {
} from '@googlemaps/react-native-navigation-sdk';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import usePermissions from '../checkPermissions';
import SelectDropdown from 'react-native-select-dropdown';
import Snackbar from 'react-native-snackbar';
import { getRouteToken, type RoutesApiTravelMode } from '../helpers/routesApi';
import { getRouteToken } from '../helpers/routesApi';

// Fixed locations for the route token example
const ORIGIN_LOCATION: LatLng = {
Expand All @@ -67,7 +66,6 @@ const RouteTokenScreen = () => {
);
const [navigationViewController, setNavigationViewController] =
useState<NavigationViewController | null>(null);
const [travelMode, setTravelMode] = useState<TravelMode>(TravelMode.DRIVING);
// API key for Routes API
const [apiKey, setApiKey] = useState<string>('');
const [isFetchingToken, setIsFetchingToken] = useState<boolean>(false);
Expand All @@ -76,30 +74,6 @@ const RouteTokenScreen = () => {
const { navigationController, addListeners, removeListeners } =
useNavigation();

const travelModeOptions = [
{ label: 'Driving', value: TravelMode.DRIVING },
{ label: 'Cycling', value: TravelMode.CYCLING },
{ label: 'Walking', value: TravelMode.WALKING },
{ label: 'Two Wheeler', value: TravelMode.TWO_WHEELER },
{ label: 'Taxi', value: TravelMode.TAXI },
];

// Map TravelMode to Routes API travel mode
const getRoutesApiTravelMode = (mode: TravelMode): RoutesApiTravelMode => {
switch (mode) {
case TravelMode.CYCLING:
return 'BICYCLE';
case TravelMode.WALKING:
return 'WALK';
case TravelMode.TWO_WHEELER:
return 'TWO_WHEELER';
case TravelMode.DRIVING:
case TravelMode.TAXI:
default:
return 'DRIVE';
}
};

const handleFetchRouteToken = useCallback(async () => {
if (apiKey.trim() === '') {
Alert.alert('Missing API Key', 'Please enter your Google Maps API key.');
Expand All @@ -108,18 +82,14 @@ const RouteTokenScreen = () => {

setIsFetchingToken(true);
try {
const tokens = await getRouteToken(
const token = await getRouteToken(
apiKey.trim(),
[ORIGIN_LOCATION, DESTINATION_LOCATION],
{ travelMode: getRoutesApiTravelMode(travelMode) }
ORIGIN_LOCATION,
DESTINATION_LOCATION
);

if (tokens.length > 0) {
setRouteTokenInput(tokens[0]!);
showSnackbar('Route token fetched successfully');
} else {
Alert.alert('No Route Found', 'The Routes API returned no routes.');
}
setRouteTokenInput(token);
showSnackbar('Route token fetched successfully');
} catch (error) {
console.error('Error fetching route token:', error);
Alert.alert(
Expand All @@ -129,7 +99,7 @@ const RouteTokenScreen = () => {
} finally {
setIsFetchingToken(false);
}
}, [apiKey, travelMode]);
}, [apiKey]);

const handleSetRouteToken = useCallback(() => {
if (routeTokenInput.trim() === '') {
Expand Down Expand Up @@ -159,7 +129,7 @@ const RouteTokenScreen = () => {

const routeTokenOptions: RouteTokenOptions = {
routeToken: confirmedRouteToken,
travelMode: travelMode,
travelMode: TravelMode.DRIVING, // Route tokens only support driving mode.
};

try {
Expand All @@ -175,12 +145,7 @@ const RouteTokenScreen = () => {
);
}
}
}, [
navigationViewController,
confirmedRouteToken,
travelMode,
navigationController,
]);
}, [navigationViewController, confirmedRouteToken, navigationController]);

const onNavigationMapReady = useCallback(async () => {
console.log(
Expand Down Expand Up @@ -328,27 +293,6 @@ const RouteTokenScreen = () => {
</Text>
</View>

<View style={CommonStyles.inputContainer}>
<Text style={CommonStyles.label}>Travel Mode:</Text>
<SelectDropdown
data={travelModeOptions}
onSelect={selectedItem => setTravelMode(selectedItem.value)}
defaultValue={travelModeOptions[0]}
renderButton={selectedItem => (
<View style={styles.dropdownButton}>
<Text style={styles.dropdownButtonText}>
{selectedItem?.label || 'Select travel mode'}
</Text>
</View>
)}
renderItem={item => (
<View style={styles.dropdownItem}>
<Text style={styles.dropdownItemText}>{item.label}</Text>
</View>
)}
/>
</View>

<View style={CommonStyles.buttonContainer}>
{isFetchingToken ? (
<ActivityIndicator size="large" color="#4285F4" />
Expand Down Expand Up @@ -399,7 +343,7 @@ const RouteTokenScreen = () => {
<View style={CommonStyles.infoContainer}>
<Text style={CommonStyles.infoTitle}>Note:</Text>
<Text style={CommonStyles.infoText}>
The travel mode must match what was used to generate the token.
Route tokens only support driving mode.
{'\n\n'}
The user location will be simulated at the origin when navigation
starts.
Expand Down Expand Up @@ -465,26 +409,6 @@ const styles = StyleSheet.create({
justifyContent: 'space-evenly',
marginBottom: 10,
},
dropdownButton: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 12,
},
dropdownButtonText: {
fontSize: 16,
color: '#333',
},
dropdownItem: {
paddingHorizontal: 16,
paddingVertical: 12,
},
dropdownItemText: {
fontSize: 16,
color: '#333',
},
sectionContainer: {
marginBottom: 16,
},
Expand Down
Loading