Mobility On-Demand Technical Solution Paper

The Passenger App

A key component in a ride-sharing product is a passenger app that customers install on Android and iOS mobile devices.

A typical workflow for the interaction between the passenger app and the backend is as follows:
  • Authenticate the customer in the app.
  • Display a map to allow a customer to indicate where they are.
  • Determine passenger location based on the customer selection on the map.
  • Search for addresses and places for dropoff locations.
  • Send a request for pickup to backend.
  • Calculate ETAs of nearby drivers in backend and select an appropriate driver.
  • Send options from backend to passenger app.
  • Select option and request fare estimate from backend.
  • Calculate route and possible toll costs in backend.
  • Send fare estimate from backend to passenger app.
The figure below illustrates the elements in the workflow related to the HERE services.
Figure 1. Request Pickup Time and Fare Request Pickup Time and Fare Sequence Diagram

Showing A Map

This section shows you how to display a map to allow customers to select their location on first an Android mobile device and then an iOS mobile device.

The following sample illustrates the process in Android.

com.here.android.tutorial;

import android.app.Activity;
import android.os.Bundle;

import com.here.android.mpa.common.GeoCoordinate;
import com.here.android.mpa.common.OnEngineInitListener;
import com.here.android.mpa.mapping.Map;
import com.here.android.mpa.mapping.MapFragment;

public class BasicMapActivity extends Activity {

  // map embedded in the map fragment
  private Map map = null;

  // map fragment embedded in this activity
  private MapFragment mapFragment = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Search for the map fragment to finish setup by calling init().
    mapFragment = (MapFragment)getFragmentManager().findFragmentById(R.id.mapfragment);
    mapFragment.init(new OnEngineInitListener() {
      @Override
      public void onEngineInitializationCompleted(OnEngineInitListener.Error error)
      {
        if (error == OnEngineInitListener.Error.NONE) {
          // retrieve a reference of the map from the map fragment
          map = mapFragment.getMap();
          // Set the map center to the Vancouver region (no animation)
          map.setCenter(new GeoCoordinate(49.196261, -123.004773, 0.0), 
            Map.Animation.NONE);
          // Set the zoom level to the average between min and max
          map.setZoomLevel((map.getMaxZoomLevel() + map.getMinZoomLevel()) / 2);
        } else {
          System.out.println("ERROR: Cannot initialize Map Fragment");
        }
      }
    });
  }
}

The following sample illustrates the process in iOS.

When developing for iOS, first drop a View into your Storyboard, then change its class from UIView to NMAMapView, and create an outlet to the view in your ViewController. The following codeblock shows how to perform basic interaction with the NMAMapView object.

#import "HelloMapViewController.h"
#import <NMAKit/NMAKit.h>

@interface HelloMapViewController ()
@property (weak, nonatomic) IBOutlet NMAMapView *mapView;
@end

@implementation HelloMapViewController

- (void)viewDidLoad
{
  [super viewDidLoad];
  [NMAMapView class];
  //set geo center
  NMAGeoCoordinates *geoCoordCenter = [[NMAGeoCoordinates alloc] initWithLatitude:49.260327 longitude:-123.115025];
  [self.mapView setGeoCenter:geoCoordCenter withAnimation:NMAMapAnimationNone];
  self.mapView.copyrightLogoPosition = NMALayoutPositionBottomCenter;

  //set zoom level
  self.mapView.zoomLevel = 13.2;
}

- (void)didReceiveMemoryWarning
{
  [super didReceiveMemoryWarning];
}

@end

For more information on the basics of displaying maps in Android and iOS mobile devices, see the Android and iOS Mobile Starter SDK Developer Guides on Android & iOS SDKs.

Determining Passenger Location

This section shows you how to turn customer geocoordinates based on their selections in a map into an address on first an Android mobile device and then an iOS mobile device.

The following sample illustrates the process in Android.

// Implementation of ResultListener
class ReverseGeocodeListener implements ResultListener<Address> {
  @Override
  public void onCompleted(Address data, ErrorCode error) {
    if (error != ErrorCode.NONE) {
      // Handle error
      ...
    } else {
      // Process result data
      ...
    }
  }
}

// Instantiate a GeoCoordinate object
GeoCoordinate vancouver = new GeoCoordinate(49.2849, -123.1252);

//Example code for creating ReverseGeocodeRequest
ResultListener<Address> listener = new ReverseGeocodeListener();
ReverseGeocodeRequest request = new ReverseGeocodeRequest(vancouver);
if (request.execute(listener) != ErrorCode.NONE) {
  // Handle request error
  ...
}

The following sample illustrates the process in iOS.

// Implementation of NMAResultListener
@interface NMAReverseGeocodeTest : NSObject<NMAResultListener> {
}
@implementation NMAReverseGeocodeTest

// NMAResultListener protocol callback implementation
- (void)request:(NMARequest*)request
  didCompleteWithData:(id)data
  error:(NSError*)error
{
  if ( ( [request isKindOfClass:[NMAReverseGeocodeRequest class]]) ) &&
    ( error.code == NMARequestErrorNone ) )
  {
    // Process result NSArray of NMAReverseGeocodeResult objects
    [self processResult:(NSMutableArray *)data];
  }
  else
  {
    // Handle error
    ...
  }
}

- (void) startSearch
{
  // Instantiate an Address object
  NMAGeoCoordinates* vancouver = [[NMAGeoCoordinates alloc] initWithLatitude:49.2849 longitude:-123.1252];

  NMAReverseGeocodeRequest* request = [[NMAGeocoder sharedGeocoder] createReverseGeocodeRequestWithGeoCoordinates:vancouver];

  NSError* error = [request startWithListener:self];
  if (error.code != NMARequestErrorNone)
  {
    // Handle request error
    ...
  }
}

@end

For more information on working with addresses and geocoordinates, see the Android and iOS Mobile Starter SDK Developer's Guides on Android & iOS SDKs.

Searching For Addresses and Place Names

This section shows how your passenger app can allow customers to search for pickup and dropoff locations on first an Android mobile device and then an iOS mobile device.

The following sample illustrates the process in Android.

// Example Search request listener
class SearchRequestListener implements ResultListener<DiscoveryResultPage> {
@Override public void onCompleted(DiscoveryResultPage data, ErrorCode error) {
  if (error != ErrorCode.NONE) {
    // Handle error
    ...
  } else {
    // Process result data
    ...
    }
  }
}

// Create a request to search for restaurants in Seattle
try {
  GeoCoordinate seattle = new GeoCoordinate(47.592229, -122.315147);

  DiscoveryRequest request = new SearchRequest("restaurant").setSearchCenter(seattle);

  // limit number of items in each result page to 10
  request.setCollectionSize(10);

  ErrorCode error = request.execute(new SearchRequestListener());
  if( error != ErrorCode.NONE ) {
    // Handle request error
    ...
  }
} catch (IllegalArgumentException ex) {
  // Handle invalid create search request parameters
  ...
}

The following sample illustrates the process in iOS.

// Sample Search request listener
@interface NMASearchTest : NSObject<NMAResultListener> {
  NMADiscoveryPage* _result;
}
@implementation NMASearchTest

// NMAResultListener protocol callback implementation
- (void)request:(NMARequest*)request
  didCompleteWithData:(id)data
        error:(NSError*)error
{
  if ( ( [request isKindOfClass:[NMADiscoveryRequest class]]) ) &&
    ( error.code == NMARequestErrorNone ) )
  {
    // Process result NMADiscoveryPage objects
    _result = (NMADiscoveryPage*) data;
  }
  else
  {
    // Handle error
    ...
  }
}
- (void) startSearch
{
  // Create a request to search for restaurants in Vancouver
  NMAGeoCoordinates* vancouver =
  [[NMAGeoCoordinates alloc] initWithLatitude:48.263392
          longitude:-123.12203];

  NMADiscoveryRequest* request =
  [[NMAPlaces sharedPlaces] createSearchRequestWithLocation:vancouver
          query:@"restaurant"]];

  // optionally, you can set a bounding box to limit the results within it.
  NMAGeoCoordinates *boundingTopLeftCoords = [[NMAGeoCoordinates alloc] 
    initWithLatitude:49.277484 longitude:-123.133693];
  NMAGeoCoordinates *boundingBottomRightCoords = [[NMAGeoCoordinates alloc] 
    initWithLatitude:49.257209 longitude:-123.11275];
  NMAGeoBoundingBox *bounding = [[NMAGeoBoundingBox alloc] 
    initWithTopLeft:boundingTopLeftCoords bottomRight:boundingBottomRightCoords];

  request.viewport = bounding;

  // limit number of items in each result page to 10
  request.collectionSize = 10;

  NSError* error = [request startWithListener:self];
  if (error.code != NMARequestErrorNone)
  {
    // Handle request error
    ...
  }
}

For more information on the search function, see the Android and iOS Mobile Starter SDK Developer's Guides on Android & iOS SDKs.

Determining Available Drivers

Once customers have selected their pickup and dropoff locations, the passenger app needs to send those geocoordinates to your backend (this step is not shown in the sample code below). At this point, the backend needs to determine which drivers best fit the request from the customer. While the business logic may vary, it typically involves determining which drivers are within a predefined drive-time radius around the customer and what their ETAs are to the pickup location.

Note: The sample code below illustrates a filtering process that only excludes drivers from consideration based upon their administrative area. For sample code that shows how to implement an additional level of filtering to further reduce the amount of time it takes to determine driver ETAs to the pickup location by reducing the number of drivers considered, see Additional Filtering of Drivers.
To request a list of ETAs for drivers based upon their locations and the designated pickup location:
  1. Send a request to the resource calculatematrix in the Routing API with the following query parameters:
    • destination with the WGS 84 compliant latitude and longitude geocoordinates of the passenger pickup location.
    • mode with the value set to fastest;car;traffic:enabled, which indicates the matrix calculation response includes routes calculated with the fastest option for routes for cars including the effect of traffic information.
    • start with a list of driver geocoodinates in WGS 84 compliant latitude and longitude pairs. You cannot specify more than 100 sets of driver geocoordinates.

      Since the length of time it takes to receive a matrix route response depends on the number of drivers for whom you request an ETA, you may wish to filter out inappropriate drivers before you construct this request. There can be several levels of filtering, including driver feedback (internal business logic), current administrative area of the driver and pickup location. For more information on filtering by administrative area, see Filtering By Administrative Area. For more information on filtering by travel time to the pickup point, see Additional Filtering of Drivers.

    • summaryAttributes with the value traveltime to have the response include travel time information for the routes.

      These travel times are your driver ETAs.

  2. Handle the response.
The following code block shows how to calculate ETAs for nearby drivers:
const https = require('https');
const _ = require('lodash');

/**
 * Builds a GET request for the Routing API matrix routing resource.
 * 
 * starts    An array of locations that serve as start points
 * destinations  An array of locations that serve as destination points
 * mode      The routing mode (e.g. 'fastest;car;traffic:enabled')
 */
function buildEtaMatrixRoutingRequestOptions(starts, destinations, mode) {
  var startParams = _(starts).map((value, index) => {
    var key = 'start' + index;
    var val = 'geo!' + value.lat + ',' + value.lon;
    return [key, val];
  }).fromPairs();
  var destinationParams = _(destinations).map((value, index) => {
    var key = 'destination' + index;
    var val = 'geo!' + value.lat + ',' + value.lon;
    return [key, val];
  }).fromPairs();
  var requestParams = _({
              'mode': mode,
              'summaryAttributes': 'traveltime',
              'app_id': {YOUR_APP_ID},
              'app_code': {YOUR_APP_CODE}
            }).assign(startParams.value())
              .assign(destinationParams.value())
              .map((value, key) => {
              return key + '=' + encodeURIComponent(value);
            }).join('&');
  return {
    method: 'GET',
    hostname: 'matrix.route.cit.api.here.com',
    path: ['/routing/7.2/calculatematrix.json', requestParams].join('?')
  };
}

/**
 * Calls the Routing API matrix routing resource to calculate an ETA Matrix
 * See buildEtaMatrixRoutingRequestOptions for an explanation of the parameters
 */
function getEtaMatrix(starts, destinations, mode) {
  return new Promise((fulfill, reject) => {
    var options = buildEtaMatrixRoutingRequestOptions(starts, destinations, mode);
    var etaMatrix = [];
    var req = https.request(options, (res) => {
      var data = "";
      res.on('data', (d) => {
        data += d;
      });
      res.on('end', () => {
        if(res.statusCode >= 400) {
          reject(new Error(data));
        }
        else
        {
          var json = JSON.parse(data);
          if(json.response && json.response.matrixEntry) {
            etaMatrix = json.response.matrixEntry.map((element) => {
              return {startIndex: element.startIndex,
                  destinationIndex: element.destinationIndex, 
                  eta: element.summary.travelTime};
            });
          }
          fulfill(etaMatrix);
        }
      })
    });
    req.on('error', (err) => {
      reject(err);
    });
    
    req.end();
  });
}

// Array of example drivers
var exampleDrivers = [
  { name: "Bob J.", location: { lat: 37.868943, lon: -122.267870 } },
  { name: "Bill F.", location: { lat: 37.806119, lon: -122.270636 } },
  { name: "Lisa M.", location: { lat: 37.727691, lon: -122.158144 } }
];

// Coordinates for downtown Berkeley, CA
var downtownBerkeley = { lat: 37.870242, lon: -122.268234 };

// Routing mode (fastest car route accounting for traffic)
var mode = 'fastest;car;traffic:enabled';

// Extract coordinates from drivers
var starts = exampleDrivers.map((driver) => {
        return { lat: driver.latitude, lon: driver.longitude };
      });

// Only one destination
var destinations = [ downtownBerkeley ];

getEtaMatrix(starts, destinations, mode)
  .then(console.log)
  .catch(console.error);
/** Output: 
  [ { startIndex: 0, destinationIndex: 0, eta: 83 },
  { startIndex: 1, destinationIndex: 0, eta: 1100 },
  { startIndex: 2, destinationIndex: 0, eta: 2100 } ]
 */
For more information on the resource calculatematrix in the Routing API, see API Reference.

Determining Toll Costs

Once your backend has determined the ETAs for the relevant drivers, you need to determine how much toll a potential driver needs to pay to determine a reliable fare estimate.

To determine potential toll costs:
  1. Send a request to the Toll Cost Extension API with the following parameters:
    • currency 3 characters (ISO 4217) currency code for the currency used in the response (for instance USD).
    • mode with the value set to fastest;car;traffic:enabled, which indicates the matrix calculation response includes routes calculated with the fastest option for routes for car, including the effect of traffic information.
    • start_ts Start timestamp of the route.
    • vspec with the characteristics of the vehicle: tollVehicleType, trailerType, trailersCount, vehicleNumberAxles, trailerNumberAxles, hybrid, emissionType, height, trailerHeight, vehicleWeight, limitedWeight, disabledEquipped, minimalPollution, hov, passengersCount, tiresCount, commercial, shippedHazardousGoods, heightAbove1stAxle.

      For instance, 2;0;0;2;0;0;5;167;0;1739;1739;0;0;0;4;0;0 with the 4 being the number of passengers.

    • waypoint0 with the WGS 84 compliant latitude and longitude geocoordinates of the driver location.
    • waypoint1 with the WGS 84 compliant latitude and longitude geocoordinates of the pickup location.
  2. Handle the response.

    The Toll Cost Extension API response includes the calculated route, along with the toll cost associated with the route, which you can then use in your estimation of the trip's total fare.

The sample code below shows how to use the Toll Cost Extension API to calculate a route and its associated toll cost:
const https = require('https');
const _ = require('lodash');

/**
 * Builds a GET request for the calculateroute.json endpoint of the Toll Cost Extension API.
 * The calculateroute.json endpoint calculates a route given two (or more) waypoints, and returns a route, along with its associated toll costs
 * 
 * start    The start location
 * destination  The destination location
 * mode     The routing mode (e.g. 'fastest;car;traffic:enabled')
 * currency   The currency in which to return toll cost information (three-letter code, e.g. 'USD')
 * vspec    A string describing the vehicle's attributes. See the Toll Cost Extension's API Reference for more information on this parameter
 */
function buildTollCostCalculateRouteRequestOptions(start, destination, mode, currency, vspec) {
  var requestParams = _({
              'waypoint0': start.lat + ',' + start.lon,
              'waypoint1': destination.lat + ',' + destination.lon,
              'mode': mode,
              'vspec': vspec,
              'currency': currency,
              'app_id': {YOUR_APP_ID},
              'app_code': {YOUR_APP_CODE}
            }).map((value, key) => {
              return key + '=' + encodeURIComponent(value);
            }).join('&');
  return {
    method: 'GET',
    hostname: 'tce.cit.api.here.com',
    path: ['/2/calculateroute.json', requestParams].join('?')
  };
}

/**
 * Calls the Toll Cost Extension API to calculate a route between waypoints and its
 * associated toll costs
 * See buildTollCostCalculateRouteRequestOptions for an explanation of the parameters.
 */
function costForWaypoints(start, destination, mode, currency, vspec) {
  return new Promise((fulfill, reject) => {
    var options = buildTollCostCalculateRouteRequestOptions(start, destination, 
      mode, currency, vspec);
    var req = https.request(options, (res) => {
      var data = "";
      res.on('data', (d) => {
        data += d;
      });
      res.on('end', () => {
        if(res.statusCode >= 400) {
          reject(new Error(data));
        }
        else
        {
          var json = JSON.parse(data);
          var costs = json.costs;
          var totalCost;
          if(costs) {
            totalCost = costs.totalCost;
          }
          fulfill(totalCost);
        }
      })
    });
    req.on('error', (err) => {
      reject(err);
    });

    req.end();
  });
}


// Coordinates for downtown Berkeley, CA
var downtownBerkeley = { lat: 37.870242, lon: -122.268234 };
// Coordinates for Union Square (in San Francisco, CA)
var unionSquare = { lat: 37.787526, lon: -122.407603 };
// Routing mode (fastest car route accounting for traffic)
var mode = 'fastest;car;traffic:enabled';
// The currency to use for the Toll Cost Extension
var currency = 'USD';
// Number of passengers
var numPassengers = 2;
// Vehicle specification, refer to TCE documentation for format information
var vehicleSpec = '2;0;0;2;0;0;5;167;0;1739;1739;0;0;0;'+numPassengers+';4;0;0';
// Calculate cost for waypoints
costForWaypoints(downtownBerkeley, unionSquare, mode, currency, vehicleSpec)
  .then(console.log)
  .catch(console.error);
/** Output:
  4.0
 */

For more information, see API Reference.