React Native Taxi App: Drawing a Route.
Cristian Echeverria
Posted on September 11, 2021
Previously we added the basic logic for the Booking Information. We displayed a modal where the user can enter the destination address and using Places API from Google. We show an array of predictions using the FlatList component from React Native.
When the user presses one of the predictions, we will draw a route using Polygon and other handy features, so let's dive into it.
Dispatching Destination Place
We need to create a dispatch action for setting the destination place when we press one of the predictions. Remember that we use a Places Manager Context Provider. Let's open it src/context/PlacesManager.js
:
export const placeReducer = (prevState, action) => {
switch (action.type) {
case 'SET_CURRENT_PLACE':
...
š case 'SET_DESTINATION_PLACE':
return {
...prevState,
destinationPlace: {
description: action.description,
placeId: action.placeId,
},
};
}
};
We update the destinationPlace
object with the prediction description
and placeId
that the user select.
Now, let's move into our Prediction component (src/components/Prediction.js
) and use dispatchPlace
function from our Places Manager Context Provider.
import React from 'react';
import {TouchableOpacity} from 'react-native';
import styled from 'styled-components/native';
import {usePlace} from '../context/PlacesManager'; š
const Text = styled.Text`
padding: 5px;
font-size: 14px;
`;
export default function Prediction({description, place_id}) {
const {dispatchPlace} = usePlace(); š
return (
<TouchableOpacity
key={place_id}
testID={`prediction-row-${place_id}`}
onPress={() => {
š dispatchPlace({
type: 'SET_DESTINATION_PLACE',
description,
placeId: place_id,
});
}}>
<Text>{description}</Text>
</TouchableOpacity>
);
}
We need to import the usePlace
hook into the Prediction component so the user can select a prediction and update the destinationPlace
object using the dispatchPlace
function.
Once we select a prediction, we update the destinationPlace and also we need to close the modal. For that reason let's pass the toggleModal
function prop into our Predictions component. Open SearchAddressModal component
const renderPredictions = ({item}) => (
<Prediction {...item} toggleModal={toggleModal} />
);
And Predictions component.
export default function Prediction({
description,
place_id,
toggleModal š
}) {
const {dispatchPlace} = usePlace();
return (
<TouchableOpacity
key={place_id}
testID={`prediction-row-${place_id}`}
onPress={() => {
dispatchPlace({
type: 'SET_DESTINATION_PLACE',
description,
placeId: place_id,
});
toggleModal(); š
}}>
<Text>{description}</Text>
</TouchableOpacity>
);
}
If everything is okay, you should see the selected Destination Place.
Draw Route in the Map.
Now that we have the information for currentPlace
and destinationPlace
, we can draw a route in the Map.
We use @mapbox/polyline library to draw the route combined with directions API.
First, let's install mapbox/polyline in our app
npm install @mapbox/polyline
Second, let's create a new utility function inside src/utils/index.js
import PoliLyne from '@mapbox/polyline';
...
export const fetchRoute = async (originPlaceId, destinationPlaceId) => {
try {
const res = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=place_id:${originPlaceId}&destination=place_id:${destinationPlaceId}&key=${GOOGLE_MAPS_API_KEY}`,
);
const json = await res.json();
if (!json.routes[0]) {
return;
}
const points = PoliLyne.decode(json.routes[0].overview_polyline.points);
const coordinates = points.map((point) => ({
latitude: point[0],
longitude: point[1],
}));
return coordinates;
} catch (error) {
console.log(error);
}
};
Directions API
Don't forget to activate Directions API inside your console.cloud.google.com project as I show in the next image
Last step, let's open our UserScreen component and use the fetchRoute
function we just added and the Polyline component from react-native-maps.
import React, {useEffect, useState, useRef} from 'react';
import {StatusBar, Platform, Image} from 'react-native';
import styled from 'styled-components/native';
š import MapView, {PROVIDER_GOOGLE, Polyline, Marker} from 'react-native-maps';
import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions';
import Geolocation from 'react-native-geolocation-service';
import {customStyleMap, MenuButtonLeft} from '../styles';
import FeatherIcon from 'react-native-vector-icons/Feather';
import DepartureInformation from '../components/DepartureInformation';
import Geocoder from 'react-native-geocoding';
import {usePlace} from '../context/PlacesManager';
import {GOOGLE_MAPS_API_KEY} from '../utils/constants';
import marker from '../assets/icons-marker.png';
import BookingInformation from '../components/BookingInformation';
import {useShowState} from '../hooks';
š import {fetchRoute} from '../utils';
...
const UserScreen = ({navigation}) => {
const [location, setLocation] = useState(null);
const {
place: {currentPlace, destinationPlace}, š
dispatchPlace,
} = usePlace();
const [showBooking, toggleShowBookingViews] = useShowState(false);
š const [polilyneCoordinates, setPolilyneCoordinates] = useState([]);
š const mapRef = useRef(null);
const handleLocationPermission = async () => {
...
};
useEffect(() => {
handleLocationPermission();
}, []);
useEffect(() => {
...
}, [dispatchPlace]);
const onRegionChange = ({latitude, longitude}) => {
...
};
useEffect(() => {
...
}, [navigation]);
š
useEffect(() => {
if (currentPlace.placeId && destinationPlace.placeId) {
fetchRoute(currentPlace.placeId, destinationPlace.placeId).then(
results => {
setPolilyneCoordinates(results);
mapRef.current.fitToCoordinates(results, {
edgePadding: {left: 20, right: 20, top: 40, bottom: 60},
});
},
);
}
}, [currentPlace, destinationPlace.placeId]);
return (
<Container>
<StatusBar barStyle="dark-content" />
{location && (
<MapView
testID="map"
š ref={mapRef}
style={mapContainer}
provider={PROVIDER_GOOGLE}
initialRegion={{
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
onRegionChangeComplete={onRegionChange}
showsUserLocation={true}
customMapStyle={customStyleMap}
paddingAdjustmentBehavior="automatic"
showsMyLocationButton={true}
showsBuildings={true}
maxZoomLevel={17.5}
loadingEnabled={true}
loadingIndicatorColor="#fcb103"
loadingBackgroundColor="#242f3e">
š {polilyneCoordinates.length > 1 && (
š <Polyline
testID="route"
coordinates={polilyneCoordinates}
strokeWidth={3}
strokeColor="#F4E22C"
/>
)}
š {polilyneCoordinates.length > 1 && (
<Marker
testID="destination-marker"
coordinate={polilyneCoordinates[polilyneCoordinates.length - 1]}
/>
)}
</MapView>
)}
š {destinationPlace.placeId === '' && (
<FixedMarker testID="fixed-marker">
<Image style={markerStyle} source={marker} />
</FixedMarker>
)}
{showBooking ? (
<BookingInformation />
) : (
<DepartureInformation toggleShowBookingViews={toggleShowBookingViews} />
)}
</Container>
);
};
export default UserScreen;
A lot of things happened. First, we import Polyline component from react-native-maps to draw the PolyLines points from the fetchRoute
function.
Second we added {currentPlace, destinationPlace}
from place
object.
Third, we added polylineCoordinates
array using useState and created a local mapRef
to access MapView components utility function.
const [polilyneCoordinates, setPolilyneCoordinates] = useState([]);
const mapRef = useRef(null);
Fourth, we added a new useEffect that will call fetchRoute function if currentPlace.placeId
and destinationPlace.placeId
ins't null/false/undefined.
useEffect(() => {
if (currentPlace.placeId && destinationPlace.placeId) {
fetchRoute(currentPlace.placeId, destinationPlace.placeId).then(
results => {
setPolilyneCoordinates(results);
mapRef.current.fitToCoordinates(results, {
edgePadding: {left: 20, right: 20, top: 40, bottom: 60},
});
},
);
}
}, [currentPlace, destinationPlace.placeId]);
Once we have the array of PolyLines points, we update the polylineCoordinates
local state and call fitToCoordinates
function from MapView to update the padding of the MapView component.
Fifth, we need to pass the mapRef
into MapView and check if we have PolyLines to draw the route. If we have the route we add a Marker for the last PolyLine point.
{location && (
<MapView
...
ref={mapRef}
...
>
{polilyneCoordinates.length > 1 && (
<Polyline
testID="route"
coordinates={polilyneCoordinates}
strokeWidth={3}
strokeColor="#F4E22C"
/>
)}
{polilyneCoordinates.length > 1 && (
<Marker
testID="destination-marker"
coordinate={polilyneCoordinates[polilyneCoordinates.length - 1]}
/>
)}
</MapView>
)}
{destinationPlace.placeId === '' && (
<FixedMarker testID="fixed-marker">
<Image style={markerStyle} source={marker} />
</FixedMarker>
)}
Lastly, we add a condition to hide the Marker that we used as reference in the beginning.
Unit Tests
It's test time! š
This time will test Prediction component. Create a new test file inside src/components/__tests__/Prediction.test.js
:
import React from 'react';
import {render, fireEvent} from '@testing-library/react-native';
import Prediction from '../Prediction';
import {PlaceContext} from '../../context/PlacesManager';
describe('<Prediction />', () => {
test('is tappable', async () => {
const place = {description: 'Domkyrkan', placeId: '123'};
const mockToggleModal = jest.fn();
const dispatchPlace = jest.fn();
const {getByText} = render(
<PlaceContext.Provider value={{place, dispatchPlace}}>
<Prediction
description={place.description}
place_id={place.placeId}
toggleModal={mockToggleModal}
/>
</PlaceContext.Provider>,
);
fireEvent.press(getByText('Domkyrkan'));
expect(dispatchPlace).toHaveBeenCalled();
expect(mockToggleModal).toHaveBeenCalled();
});
});
Posted on September 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.