Mobility On-Demand Technical Solution Paper

Additional Filtering of Drivers

While the previous section showed a basic implementation of filtering drivers before presenting them to the passenger, this section illustrates an additional method of further reducing the amount of drivers for whom you calculate ETAs.

The typical workflow is much the same as in the previous section, with the additional of the following steps:
  • Backend receives request for ride with pickup and dropoff locations from passenger app.
  • Determine which drivers are within a reasonable range of the pickup location.
  • Calculate ETAs of nearby drivers in backend and select an appropriate driver - covered in the next section.
The figure below illustrates the elements in the workflow related to the HERE services.
Figure 1. Assign Driver To Passenger Assign Driver To Passenger Sequence Diagram

Determining Reasonable Travel Time Areas To Pickup Location

In this case, this flow starts with a passenger sending the pickup and dropoff locations to your backend using your passenger app. Using that information, you can use the resource calculateisoline in the Routing API to calculate one or multiple reverse isochrone shapes around the passenger's pickup location. This allows you to determine within which polygonal shape a driver has to be in order to reach the pickup location within a certain amount of minutes. You use this logic to filter out which drivers you exclude from the calculate ETA request.

To calculate isolines around the passenger pickup location:
  1. Send a request to the resource calculateisoline in the Routing API with the following query parameters:
    • destination with a value indicating the WGS 84 compliant latitude and longitude geocoordinates of the pickup location.
    • rangetype set to time to indicate you want the response to be based on how long it takes to get to the specified location.
    • range set to the desired time for the driver to arrive (or a comma-separated list of multiple desired times) in seconds.

      The amount of time you wish to allow drivers to reach the pickup location depends on your business concepts. Perhaps you may wish to have arrival times arranged in increasing steps, for example.

The sample code below shows how to construct and perform a query for a reverse isochrone:
const https = require('https');
const _ = require('lodash');

/**
 * Builds a GET request for the Isoline Routing API.
 * An isoline is a shape that represents the area which can be reached from a certain point
 * within a given time or distance (regular isoline), or the area from which a certain
 * point can be reached within a given time or distance (reverse isoline)
 * 
 * rangeType  Indicates whether the isoline represents distance (isodistance) or time (isochrone).  Possible values are 'distance' and 'time'
 * isReverse  Indicates whether a regular or reverse isoline will be calculated.  'true' results in a reverse isoline, 'false' in a regular isoline
 * location   The location around which to build the isoline
 * range    The distance (in meters) or time (in seconds).  The unit is determined by the rangeType parameter
 * mode     The routing mode (e.g. 'fastest;car;traffic:enabled')
 */
function buildIsolineRoutingRequestOptions(rangeType, isReverse, location, range, mode) {
  var locationParamKey = isReverse ? 'destination' : 'start';
  var locationParam = {};
  locationParam[locationParamKey] = 'geo!' + location.lat + ',' + location.lon;
  var requestParams = _({
              'rangeType': rangeType,
              'range': range,
              'mode': mode,
              'app_id': {YOUR_APP_ID},
              'app_code': {YOUR_APP_CODE}
            }).assign(locationParam)
              .map((value, key) => {
              return key + '=' + encodeURIComponent(value);
            }).join('&');
  return {
    method: 'GET',
    hostname: 'isoline.route.cit.api.here.com',
    path: ['/routing/7.2/calculateisoline.json', requestParams].join('?')
  };
}

/**
 * Calls the Isoline Routing API to calculate a reverse isochrone
 * See buildIsolineRoutingRequestOptions for an explanation of the parameters
 *
 * Returns the isolines in WKT format (for use with other APIs, such as Geofencing Extension)
 */
function getReverseIsochrone(location, range, mode) {
  return new Promise((fulfill, reject) => {
    var options = buildIsolineRoutingRequestOptions('time', true, location, range, mode);
    var wkt = "";
    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 the response contains one or more isolines, convert them to a
          // WKT MULTIPOLYGON
          if(json.response && json.response.isoline) {
            wkt = "MULTIPOLYGON (" + json.response.isoline.map((isoline) => {
              return isoline.component.map((component) => {
                return "((" + component.shape.map((shape) => {
                  // Reverse order of latitude and longitude, WKT expects
                  // them in the format X Y, where X is longitude and Y 
                  // is latitude
                  return shape.split(',').reverse().join(' ');
                }).join(', ') + "))";
              }).join(', ');
            }).join(', ') + ")";
          }
          fulfill(wkt);
        }
      })
    });
    req.on('error', (err) => {
      reject(err);
    });
    
    req.end();
  });
}

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

// Isochrone range in seconds
var range = 300;

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

getReverseIsochrone(downtownBerkeley, range, mode)
  .then(console.log)
  .catch(console.error);
/** Output:
  MULTIPOLYGON (((-122.2859859 37.8705597, -122.2833252 37.8705597, ... , 
  -122.2859859 37.8705597)))
 */
For more information, see the API Reference.

Determiming Which Drivers Are Inside the Reverse Isochrone Shapes

Once you have determined the areas where drivers can reach the pickup location within a reasonable time, you need to load these areas to the Geofencing Extension API and use this API to check whether drivers are inside or entering the shape based on the location reported by their driver app.

To upload the shapes from the previous step:

  1. Convert the polygon into a Shapefile or WKT text file.
  2. Compress the result to a zip archive.
  3. Upload the zip archive to the Geofencing Extension API using a POST request to the resource layers and using the parameter layer_id to assign an identifier to the shape (or shapes).
    Note: Batch several shapes together into a layer, as the number of layers you can use with the Geofencing Extension is limited. Each polygon should also have a unique ID within the layer to identify it. For example, when uploading a WKT file, add an attribute column that contains such an ID (which can be an ID that you use in your database to keep track of the ride this shape is associated with).
The sample code below demonstrates how to perform the above steps, excluding the first step (for which you can refer to the reverse isochrone calculation code sample):
const https = require('https');
const _ = require('lodash');
const FormData = require('form-data');
const zip = require('node-zip')();

/**
 * Builds a POST request for uploading a layer to the Geofencing Extension.
 * 
 * layerId  The id of the layer being uploaded
 * form   A FormData object containing the file to be uploaded
 */
function buildUploadLayerRequestOptions(layerId, form) {
  var requestParams = _({
              'layer_id': layerId,
              'app_id': {YOUR_APP_ID},
              'app_code': {YOUR_APP_CODE}
            }).map((value, key) => {
              return key + '=' + encodeURIComponent(value);
            }).join('&');
  return {
    method: 'POST',
    hostname: 'cle.cit.api.here.com',
    path: ['/2/layers/upload.json', requestParams].join('?'),
    headers: form.getHeaders()
  };
}

/**
 * Calls the Geofencing Extension API to upload a WKT file to a custom layer.
 * See buildUploadLayerRequestOptions for an explanation of the parameters
 */
function uploadWkt(layerId, wkt) {
  return new Promise((fulfill, reject) => {
    // The Geofencing Extension expects the data as "multipart/form-data" MIME-type, 
    // hence we use the FormData module to append the data
    var form = new FormData();

    // The data must be zipped before uploading to the Geofencing Extension
    // The WKT filename is arbitrary, but must have the .wkt extension
    zip.file('wktUpload.wkt', wkt);
    var data = zip.generate({type: 'base64',compression:'DEFLATE'});
    var buffer = new Buffer(data, 'base64');

    // The file must be appended as 'zipfile'
    form.append('zipfile', buffer, {filename: 'wktUpload.wkt.zip', 
      contentType: 'application/zip'});

    var options = buildUploadLayerRequestOptions(layerId, form);
    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
        {
          fulfill();
        }
      })
    });
    form.pipe(req);
    req.on('error', (err) => {
      reject(err);
    });

    req.end();
  });
}

/** Example WKT:
 *  ID  WKT
 *  1   MULTIPOLYGON (((-122.2859859 37.8705597, -122.2833252 37.8705597, ... , 
  -122.2859859 37.8705597)))
 */
var wkt = "ID\tWKT\n1\tMULTIPOLYGON (((-122.2859859 37.8705597, 
  -122.2833252 37.8705597, ... , 
  -122.2859859 37.8705597)))"

// Example layer id
var layer = 1;

uploadWkt(layer, wkt)
  .then(() => { console.log("Uploaded WKT layer") })
  .catch(console.error)
/** Output:
  Uploaded WKT layer
 */ 
For more information, see API Reference.

Checking if a Driver Is In a Passenger Area

Once you have uploaded the isoline shapes based on the time to the pickup location to the Geofencing Extension API, you can then check whether a driver is within that area.

To check whether a driver is within range of a pickup location:

  1. For currently unassigned drivers, send a request to the resource proximity in the Geofencing Extension API whenever they send a location update to your backend from your driver app. Use the following query parameters:
    • layer_ids with a comma separate list of IDs identifying the layers with shapes you have uploaded.
    • proximity with the WGS 84 compliant latitude and longitude of a driver you wish to check.
    • keyattributes with the ID of the shape you wish to check against.

    The response contains a list of geometries, the distance between them and the position previously passed in the proximity parameter. If the position is within the geometry, this distance is negative.

The following code block demonstrates how to construct and perform a Search Proximity request:
const https = require('https');
const _ = require('lodash');

/**
 * Builds a GET request for the Geofencing Extension using custom layers uploaded by the
 * developer.
 * 
 * location     The location to test for
 * layerIds     An array of ids of the layers to test the location for
 * keyAttributes  An array of the attributes in each layer which uniquely identify an entry
 *        This array must always have the same length as the layerIds array, as a 
 *        keyAttribute needs to be present for each layer
 */
function buildGfeWithCustomLayersRequestOptions(location, layerIds, keyAttributes) {
  var requestParams = _({
              'layer_ids': layerIds.join(','),
              'key_attributes': keyAttributes.join(','),
              'proximity': location.lat + ',' + location.lon,
              'app_id': {YOUR_APP_ID},
              'app_code': {YOUR_APP_CODE}
            }).map((value, key) => {
              return key + '=' + encodeURIComponent(value);
            }).join('&');
  return {
    method: 'GET',
    hostname: 'cle.cit.api.here.com',
    path: ['/2/search/proximity.json', requestParams].join('?')
  };
}

/**
 * Calls the Geofencing Extension API to find custom layers a location is within.
 * See buildGfeWithCustomLayersRequestOptions for an explanation of the parameters
 */
function searchCustomLayers(location, layerIds, keyAttributes) {
  return new Promise((fulfill, reject) => {
    var options = buildGfeWithCustomLayersRequestOptions(location, layerIds, keyAttributes);
    var rows = [];
    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 geometries = json['geometries'];
          if(geometries) {
            rows = geometries.filter((geometry) => {
              return geometry.distance <= 0;
            }).map((geometry) => {
              var keyAttribute;
              // The geometry doesn't necessarily have a layerId object if the
              // request contained only one layer
              if(geometry.layerId) {
                keyAttribute = 
                  keyAttributes[layerIds.indexOf(geometry.layerId)];
              } else {
                keyAttribute = keyAttributes[0];
              }
              return geometry.attributes[keyAttribute];
            });
          }
          fulfill(rows);
        }
      })
    });
    req.on('error', (err) => {
      reject(err);
    });

    req.end();
  });
}

// Example driver
var driver = { name: "Bob J.", location: { lat: 37.868943, lon: -122.267870 } };

// Example layer ids
var layerIds = ['ON_DEMAND_DEMO_LAYER'];

// Key attributes for layers
var keyAttributes = [ 'ID' ];

searchCustomLayers(driver.location, layerIds, keyAttributes)
  .then(console.log)
  .catch(console.error);
/** Output:
  [ 'ON_DEMAND_DEMO_LAYER' ]
 */
For more information, see the API Reference.

Determining Available Drivers

Once you have filter your drivers based on whether they are within a reasonable ETA of the passenger pickup location, then you need to calculate their ETAs to that location – just as in the previous section.

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.
    • 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.
  3. Share the driver's details and ETA with the passenger and share the passenger details and pickup location with the driver.
  4. Remove the geometry from the layer in the Geofencing Extension API the next time you upload this layer, to prevent it from matching other drivers to this ride.
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 the API Reference.

You cannot use this account to purchase a commercial plan on Developer Portal, as it is already associated to plans with different payment methods.

To purchase a commercial plan on Developer Portal, please register for or sign in with a different HERE Account.

Something took longer than expected.

The project should be available soon under your projects page.

Sorry, our services are not available in this region.

Something seems to have gone wrong. Please try again later.

We've detected that your account is set to Australian Dollars (AUD).
Unfortunately, we do not offer checkouts in AUD anymore.
You can continue using your current plan as normal, but to subscribe to one of our new plans,
please register for a new HERE account or contact us for billing questions on selfservesupport@here.com.