DEV Community

Cristian Echeverria
Cristian Echeverria

Posted on

React Native Taxi App: Drawing a Route.

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,
        },
      };
  }
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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} />
  );
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Select a destination place

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
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

Directions API

Don't forget to activate Directions API inside your console.cloud.google.com project as I show in the next image

Directions API

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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>
      )}
Enter fullscreen mode Exit fullscreen mode

Lastly, we add a condition to hide the Marker that we used as reference in the beginning.

Draw a route

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

Discussion (0)