HERE Android SDK Developer's Guide

Electronic Horizon

Electronic Horizon uses the map as a sensor to provide a continuous forecast of the upcoming road network by ingesting the map topography which is currently out of the driver’s sight. Together with the vehicle position and other relevant road attributes, the HERE Electronic Horizon API creates a simplified model of the road ahead. By using techniques like adaptive cruise control (ACC) or curve speed warning, you can help the driver to save fuel and to make driving safer and less stressful.

Types of data the HERE Electronic Horizon API can predict include:

  • Most probable path
  • Alternative side paths
  • Speed limits
  • Slopes
  • Curvature coordinates
  • Road classes
  • Road types

The HERE Electronic Horizon API fully supports the offline use case and adheres to the vehicle’s advanced driver assistance systems (ADAS) standard being ADASISv2 compliant.

An example app including source code is available for this feature. See Service Support to contact us for more details.

Before You Start

Please make sure to use the correct flavor of the HERE SDK. Electronic Horizon is included in the HERE Android SDK since version 3.6.0.

Note: Electronic Horizon supports online and offline road network prediction. Although the API is capable of accessing map data from the cloud, it is strongly recommended to download the expected map areas beforehand. All data that would be available online is fully available in offline mode.

Since the Electronic Horizon requires the user’s position, it is mandatory to require the ACCESS_FINE_LOCATION permission. Although the API can also work in offline mode, it is recommended to request at least the following permissions from the user:

  • android.permission.ACCESS_FINE_LOCATION
  • android.permission.WRITE_EXTERNAL_STORAGE
  • android.permission.ACCESS_NETWORK_STATE
  • android.permission.INTERNET
  • android.permission.ACCESS_WIFI_STATE

How to Retrieve Your First Electronic Horizon Events

Before you can access the Electronic Horizon, make sure to start the PositioningManager:

PositioningManager positioningManager = PositioningManager.getInstance();
positioningManager.start(PositioningManager.LocationMethod.GPS_NETWORK);

Then you can create the Electronic Horizon instance and set the listener.

ElectronicHorizon electronicHorizon = new ElectronicHorizon();
electronicHorizon.setListener(new ElectronicHorizon.Listener() {
  @Override
  public void onNewPosition(Position position) { }

  @Override
  public void onTreeReset() { }

  @Override
  public void onLinkAdded(PathTree path, Link link) { }

  @Override
  public void onLinkRemoved(PathTree path, Link link) { }

  @Override
  public void onPathAdded(PathTree path) { }

  @Override
  public void onPathRemoved(PathTree path) { }

  @Override
  public void onChildDetached(PathTree parent, PathTree child) { }
});

Now you are able to tell the Electronic Horizon engine to calculate the road network ahead – based on the current position of your device. You do this by simply calling:

electronicHorizon.update();
Note: Calling the update() method is resource intensive. It is advised not to call this function too frequently. PositioningManager usually provides a new position each second. Instead you may want to update the Electronic Horizon only after:
  • A certain distance
  • A certain amount of time
  • A certain event

Since calling update() can take a while, the ElectronicHorizon.Listener provides several callbacks to get notified as soon as the predicted path has changed:

  • onNewPosition(Position position): Always called at last after the other callbacks. May contain an unchanged PathTree if the position is the same as for the previous call. Always contains the full path.
  • onTreeReset(): As soon as the current position is beyond the main PathTree's look ahead or trailing distance, onTreeReset() gets called and a new PathTree object is constructed. Otherwise the user is still traveling on the expected paths and the main PathTree is instead extended.
  • onLinkAdded(PathTree path, Link link): Notifies which Link was just added to a PathTree. Note that the main PathTree does not contain a parent while a PathTree may or may not contain children, no matter how deep it is nested in the hierarchy.
  • onLinkRemoved(PathTree path, Link link): Called as soon as a Link was removed from a parent PathTree. Note that it is not guaranteed to be called for every Link that was once added to a path tree.
  • onPathAdded(PathTree path): A new PathTree is added to the existing tree. Since each PathTree contains parents and children, you could start to traverse the road network from here, although only onNewPosition() is guaranteed to provide the full path.
  • onPathRemoved(PathTree path): Called before onNewPosition() as soon as a PathTree was removed. Usually this is a good place to remove any MapObjects from the Map that belong to this PathTree.
  • onChildDetached(PathTree parent, PathTree child): A path was detached from a parent because the user has left the main path, but is still traveling on a side path. The detached child most likely becomes the new main path.

Make sure to call update() after a valid position is provided by the PositioningManager, otherwise no events are delivered. Please note that onNewPosition() is guaranteed to be called after each update() request, even if no map matched position could be found. The only requirement is a valid position which is automatically fetched by the Electronic Horizon after an update.

Note: It is recommended to call update() from a background thread as the execution may take some time.

If the user is off-road, Electronic Horizon cannot calculate a predicted path since a map matched position is required to operate successfully. This can be detected with the following code snippet:

@Override
public void onNewPosition(Position position) {
  PathTree pathTree = position.getPathTree();
  if (pathTree == null) {
    // we seem to be off-road
  }
}

A PathTree represents the road network ahead and by default also behind. It contains nested PathTree as children and parents, but only the root path does not contain a parent PathTree. This can be used to find out if a PathTree represents the root path. The path belonging to the Position is always the main path on which the user is currently traveling. This may be the root path, but it can also happen that the user decides to take a turn into a side path which then represents the new main path containing the root path as parent. When the path is detached and the tree gets restructured, the new main path becomes the root path again.

PathTree parentPathTree = pathTree.getParent();
if (parentPathTree == null) {
  // pathTree is the main path and the root path
} else {
  // pathTree is the main path, but not the root path
}

Each PathTree contains at least one Link, which represents the smallest segment on the road that can be accessed via the API. Usually the main path of a PathTree is split into several Links which can be uniquely identified by an ID. A road leg can be seen as a sequence of several Links.

Predict the Most Probable Path (MPP)

Without giving a route to follow, Electronic Horizon predicts the most probably path (which acts as root and/or main path) a user may take. Previous positions may be taken into account. If no previous position is available, the API suggests the most-likely path solely based on the available road attributes.

The probability of each path can be extracted like shown below:

@Override
public void onNewPosition(Position position) {
  PathTree pathTree = position.getPathTree();
  if (pathTree == null) {
    // off-road
    return;
  }

  // Main path is expected to have a probability of 1 (= 100%)
  float probability = pathTree.getProbability();
  Log.d(LOG_TAG, "Probability of main path: " + probability);

  for (PathTree childPathTree : pathTree.getChildren()) {
    Log.d(LOG_TAG, "Probability of side path: " + childPathTree.getProbability());
  }
}

If a PathTree is predicted with a probability of 0, then it is highly unlikely that a driver may take this turn. Nevertheless Electronic Horizon is including such roads. In some cases an application may want to present such information to the user, such as in the case of one-way streets. Note that all probabilities except for the main path are below 0.5 (= 50%) as a driver is expected to follow the main path with a higher probability.

Set a Route

Typically you want Electronic Horizon to follow a route, which then becomes the MPP. You can directly set a route to the electronic horizon instance:

Route myRoute = getRoute();
electronicHorizion.setRoute(myRoute);

There is no option to unset a route as setting a null Route would lead to an IllegalArgumentException. However, as soon as a user might leave the path of the route, the MPP is recalculated based on the new positions.

Note: Only routes with transport mode RouteOptions.TransportMode.CAR and RouteOptions.TransportMode.TRUCK are supported. Other types throw an IllegalArgumentException.

Customize the Electronic Horizon Ahead and Behind

Typically you want to define the kilometers ahead that Electronic Horizon can predict. You can do this by setting an int-Array to define up to n nested side paths.

int trailingDistanceInMeters = 200;
electronicHorizon.setTrailingDistanceInCentimeters(trailingDistanceInMeters * 100);

int mainPathDistanceInMeters = 1000;
int sidePathDistanceInMeters = 500;

// setLookAheadDistancesInCentimeters() takes an arbitrary number of values (Varargs)
electronicHorizon.setLookAheadDistancesInCentimeters(
    mainPathDistanceInMeters * 100,
    sidePathDistanceInMeters * 100);

Old Links and PathTree children are removed only when they are fully behind the defined trailing distance from the current position. If a Link is longer than the trailing distance, the link is not removed until its start and end nodes are outside the defined threshold. Therefore it may appear that setting a trailing distance might not have an immediate effect, which is especially valid for longer Links. Note that the minimum trailing distance is 1 cm.

The main path has at least one child while the full PathTree always contains n + 1 side paths. Although the driver is expected to follow the main path at each junction alternative paths can be taken. Any road that is branching from the main path is called a 1st level side path. Since side paths can further branch into side paths all side paths branching from 1st level side paths are called 2nd level side paths, and so on.

As you can see from the illustration below, the distance of each side path is starting from the vehicle's position. When three look-ahead distances are set (MPP, 1st side path, 2nd side path) then a side path of the 2nd side path is as long as one link. See the light blue arrow below as an example.

Figure 1. Trailing Distance and Look Ahead Distance

In order to understand how the root PathTree and its children are generated it may be helpful to visualize the paths programmatically on a map view as shown below for two look-ahead distances. The full path tree can be retrieved from the onNewPosition() callback:

@Override
public void onNewPosition(Position position) {
  PathTree pathTree = position.getPathTree();
  if (pathTree == null || pathTree.equals(previousPathTree)) {
    // same PathTree object, we ignore any added paths / links
    // and wait for a newly created PathTree
    return;
  }
  previousPathTree = pathTree;

  if (pathTree != null) {
    showAllLinksInPathTree(pathTree);
  } else {
    // offroad
  }
}
Note: An implementation may want to handle an added path as soon as the existing PathTree object is extended. In this example, we are only interested in a newly created PathTree object to visualize the full path tree. Keep in mind that a user is usually traveling along the expected path. This path is then extended instead of being created freshly anew. Same applies for the paths behind the trailing distance which are removed from a parent PathTree instead of being reset as part of a full PathTree reset (see onTreeReset()).

Once you can be sure to have a valid PathTree, you can start rendering all nested Links. The zIndex is only used to ensure that deeper paths are rendered on top of the higher ones in the hierarchy:

private void showAllLinksInPathTree(PathTree mainPathTree) {
   int zIndex = 0;
   addLinksToHereMap(mainPathTree.getLinks(), Color.RED, zIndex);

   // comment out the below to show only main path
   for (PathTree firstLevelSidePath : mainPathTree.getChildren()) {
     addLinksToHereMap(firstLevelSidePath.getLinks(), Color.BLUE, ++zIndex);

     for (PathTree secondLevelSidePath : firstLevelSidePath.getChildren()) {
       addLinksToHereMap(secondLevelSidePath.getLinks(), Color.GREEN, ++zIndex);

       // and so on ...
     }
   }
 }

private void addLinksToHereMap(LinkRange links, int linkColor, int zIndex) {
   for (Link link : links) {
     GeoPolyline geoPolyline = getGeoPolyLine(link);
     if (geoPolyline != null) {
       MapPolyline mapPolyline = new MapPolyline(geoPolyline);
       mapPolyline.setLineColor(linkColor);
       mapPolyline.setLineWidth(10);
       mapPolyline.setZIndex(zIndex);
       hereMap.addMapObject(mapPolyline);
     }
   }
 }

By accessing a GeoPolyline, you can render the path as a MapObject onto the map as show above. The path geometry of each Link is defined by a GeoPolyline you must extract using a MapAccessor. Since the GeoPolyline is not part of the Link itself, it must be extracted from the map data. Therefore it is mandatory to handle a DataNotReadyException:

@Nullable
 private GeoPolyline getGeoPolyLine(Link link) {
   GeoPolyline geoPolyline;
   try {
     geoPolyline = mapAccessor.getLinkPolyline(link);
   } catch (DataNotReadyException dataNotReadyException) {
     // should never happen when map data was already loaded
     return null;
   }
   return geoPolyline;
 }

At each junction a driver may decide to drive back to where she/he was coming from. These paths have a probability below 50%, but are nevertheless a valid choice and are therefore by default not excluded from Electronic Horizon's PathTree hierarchy. However, in most cases you would like to ignore unlikely turns. For example, with the following code snippet you can detect if a side path is following the parent path in opposite direction:

private boolean isBackTurn(PathTree childPathTree) {
  final PathTree parentPath = childPathTree.getParent();
  if (parentPath == null) {
    return false;
  }

  final long childPathTreeOffset = childPathTree.getOffsetCentimeters();
  for (Link parentLink : parentPath.getLinks()) {
    if (parentLink.getEndOffsetCentimeters() == childPathTreeOffset) {
      Link firstInChild = childPathTree.getLinks().iterator().next();
      return (firstInChild.getId() == parentLink.getId()) &&
          (firstInChild.getDirection() != parentLink.getDirection());
    }
  }
  return false;
}

First you compare the end offset distance of the links of the parent path with the offset of the side path (child). Then you can compare the ID of the first Link of the children with the ID of the last Link of the parent: If the direction is different, then the child must be heading in the opposite direction. Each road segment is represented by a Link which is uniquely identified by an ID. Only attributes like direction and / or offsets may be different for the same Link. Note that this is just an example, which should be further optimized and adapted to your specific needs.

If all goes well you should see the full PathTree rendered as shown below:

  • The root path is red
  • The 1st level side path is blue
  • The 2nd level side path is green
Figure 2. Rendered Root PathTree, 1st and 2nd Side Paths

It is not possible to set a look ahead distance for the n + 1 child: Therefore the green side path (which is the 2nd level side path) is only as long as one link (the first link on a side path of the blue path). Remember that we have passed only two distance values for the main path (red) and the 1st level side path (blue). By default this results in a 3rd level side path (green) that consists of one link.

Note: The PathTree you receive with the onNewPosition() callback always includes the full road network ahead including unlikely paths like, for example, one way or dead end streets. Depending on your use case, it may be useful to ignore paths with a low probability or to limit the scope to the main path only - or even to filter out Links that follow into the opposite direction like shown above. Make sure not to add too many side paths to the horizon as this would not only increase processing time, but also the likelihood of less important Links. In most cases it should be sufficient to set only one look ahead distance, as this would by default include first level side paths with a length of one Link.

Get Road Attributes

Electronic Horizon offers different road attributes that can be accessed from the available map data via a MapAccessor. Some attributes like slope data are directly accessible, while MetaData (such as side of driving or unit system) and LinkInformation (such as FormOfWay) hold multiple attributes. All attributes are valid for the entire Link.

Shown below is an example on how to extract speed limits from LinkInformation:

private void logSpeedLimits(PathTree pathTree) {
  MapAccessor mapAccessor = electronicHorizon.getMapAccessor();

  for (Link link : pathTree.getLinks()) {
    LinkInformation linkInformation;
    try {
      linkInformation = mapAccessor.getLinkInformation(link);
    } catch (DataNotReadyException dataNotReadyException) {
      return;
    }
    double speedLimitMetersPerSecond = linkInformation.getSpeedLimitMetersPerSecond();
    String speedLimitText = (int) Math.round(speedLimitMetersPerSecond * 3.6) + " Km";
    Log.d(LOG_TAG, "Speed limit: " + speedLimitText);
   }
}

One speed limit is available per Link and it is valid for the whole length of the Link. In other words: If the speed limit changes in comparison to the previous Link, the location of where the speed limit changes equals the start coordinate of the Link.

Please refer to the API reference for more road attributes (like functional road classes) which could be extracted in a similar way.

Road attributes that are not part of LinkInformation must be retrieved directly from MapAccessor. Below you can find an example on how this could be implemented for road slope data:

double lengthInMeters;
List<SlopeDataPoint> slopeDataPointList;
try {
  LinkInformation linkInformation = mapAccessor.getLinkInformation(link);
  lengthInMeters = linkInformation.getLengthMeters();
  slopeDataPointList = mapAccessor.getSlopeDataPoints(link);
} catch (DataNotReadyException dataNotReadyException) {
  return;
}

for (SlopeDataPoint slopeDataPoint : slopeDataPointList) {
  double normalizedPosition = slopeDataPoint.getRelativePositionOnLink();
  double positionOnLinkInMeters = normalizedPosition * lengthInMeters;
  double percent = slopeDataPoint.getSlopePercent();
  Log.d(LOG_TAG, "Slope data position: " + positionOnLinkInMeters);

  if (percent == Double.POSITIVE_INFINITY) {
    Log.d(LOG_TAG, "Ascending slope > 45°");
  } else if (percent == Double.NEGATIVE_INFINITY) {
    Log.d(LOG_TAG, "Downward slope > 45°");
  } else {
    if (percent > 0) {
      Log.d(LOG_TAG, "Ascending slope: " + percent);
    } else {
      Log.d(LOG_TAG, "Downward slope: " + percent);
    }
  }
}

While the raw slope data is extracted from mapAccessor.getSlopeDataPoints(link) you may also want to retrieve additional data from LinkInformation, such as the total length in meters of the current Link. A SlopeDataPoint holds only the normalized position on the Link (for example, 0.0 = Start, 0.5 = Middle, 1.0 = End). In order to find the position of the SlopeDataPoint relative to the beginning of the Link, we must multiply its position with the relative length:

double positionOnLinkInMeters = normalizedPosition * lengthInMeters;

The slope itself is given as percentage value as it would appear on a traffic sign. 100% equals a slope of 45° degrees. Although the world-wide steepest known slope is around 35° degrees, in theory it can happen that a slope is higher than 45° degrees. This is indicated by the Electronic Horizon API as positive or negative infinity as shown above. There can be no percentage value above 100% or below -100%. A positive value indicates an ascending slope while a negative value represents a downward slope. The coordinate of the slope data point can be found by comparing its relative position with the curvature data extracted from the GeoPolyline of the Link (see above).

Note: Slope data information is available only when the isolated disk cache of the device is used. This can be set by calling MapSettings.setIsolatedDiskCacheRootPath(String path, String intent) as shown here.

Retrieve ADASIS v2 Message Data

The Electronic Horizon API is capable of enriching the vehicle data by providing relevant message data for ADASIS v2 compliant in-car clients. To prepare communication to a client, you first need to provide a message configuration to define the desired types of data. As a next step, you can create an instance of the AdasisV2Engine which consumes that configuration.

AdasisV2MessageConfiguration adasisV2MessageConfiguration =
    AdasisV2MessageConfiguration.createAllEnabled();

AdasisV2Engine adasisV2Engine;
adasisV2Engine = new AdasisV2Engine(adasisV2MessageConfiguration);
adasisV2Engine.setListener(new AdasisV2Engine.Listener() {
  @Override
  public void onAdasisMessageReceived(byte[] bytes) {
    // The ADASIS v2 specification defines messages with 8 bytes payload data
    // where the first three bits define the message type followed by the actual content.
  }
});

The above creates a configuration which enables all available message types. Alternatively you can create a configuration enabling only default messages types by calling:\

AdasisV2MessageConfiguration.createDefaultsEnabled();

This enables only POSITION, STUB, SEGMENT and META-DATA message types. In total, six different message types are defined by the ADASIS v2 specification:

  • POSITION message specifies the current position(s) of the vehicle.
  • STUB message indicates the start of a new path that has an origin at an existing one.
  • SEGMENT message specifies the most important attributes of a part of the path.
  • PROFILE SHORT message describes attribute of the path whose value can be expressed in 10 bits.
  • PROFILE LONG message describes attribute of the path whose value can be expressed in 32 bits.
  • META-DATA message contains utility data.

The Electronic Horizon API provides getters and setters to dynamically enable or disable each of the messages and parts of its containing data (such as slopes) individually, like shown below for the SEGMENT message type:

if (adasisV2MessageConfiguration.isSegmentEnabled()) {
  adasisV2MessageConfiguration.setSegmentEnabled(false);
}

In order to receive data you need to set the AdasisV2Engine.Listener to the engine (see above). You can run a single AdasisV2Engine instance along with an ElectronicHorizion instance, but you have to make sure to call adasisV2Engine.update() in order to trigger a sequence of ADASIS messages based on the current device position. The usage follows the same pattern as described already for the ElectronicHorizon instance. Therefore it is required to enable Positioning before you are able to receive your first ADASIS v2 compliant messages.

ADASIS v2 assumes a one-way communication between an ADASIS v2 Horizon provider and a client. The Electronic Horizon API ensures that the receiving sequence of messages is compliant to the ADASIS v2 specification, so usually you do not need to parse its content. Note that the ADASIS specification is proprietary. To establish a communication to a client, contact your ADASIS v2 stakeholder. The AdasisV2Engine solely acts as provider and therefore does not receive or parse any information from a contributing client – as this would typically depend on the individual needs of an actual implementation.