SDK for iOS 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.

Before You Start

Please make sure to use the correct flavor of HERE SDK. Electronic Horizon is included in SDK for iOS 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. For offline usage (via ODML) make sure to enable NMAMapDataGroupADAS and NMAMapDataGroupLinkGDBIdPVId data groups before downloading the desired map data.

Since the Electronic Horizon requires the user’s position, it is mandatory to require 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:

<string>This is needed to determine your current location</string>
<string>This is needed to determine your current location</string>

How to Retrieve Your First Electronic Horizon Events

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

NMAPositioningManager* positioningManager = [NMAPositioningManager sharedPositioningManager];
if ([positioningManager startPositioning]) {
  positioningManager.mapMatchingEnabled = YES;
  positioningManager.mapMatchMode = NMAMapMatchModeCar;

Then you can create the Electronic Horizon instance and set the delegate to receive events. Make sure your delegate conforms to the NMAElectronicHorizonDelegate protocol. The protocol defines several callbacks which are marked as optional. Therefore you are only required to implement the callbacks you really need.

self.electronicHorizon = [[NMAElectronicHorizon alloc] init];
self.electronicHorizon.delegate = self;

- (void)electronicHorizon:(nonnull NMAElectronicHorizon *)electronicHorizon
  didReceiveNewPosition:(nonnull NMAEHPosition *)position {
  // handle event here

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:

[self.electronicHorizon update];
Note: Calling update method is resource intensive. It is advised not to call this function too frequently. NMAPositioningManager 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 NMAElectronicHorizonDelegate protocol provides several callbacks to get notified as soon as the predicted path has changed:

  • electronicHorizon:didReceiveNewPosition: - Always called last after the other callbacks. May contain an unchanged NMAEHPathTree if the position is the same as for the previous call. Always contains the full path.
  • electronicHorizonDidReceiveTreeReset: - As soon as the current position is beyond the main NMAEHPathTree look ahead or trailing distance, electronicHorizonDidReceiveTreeReset gets called and a new NMAEHPathTree object is constructed. Otherwise the user is still travelling on the expected paths and the main NMAEHPathTree is instead extended.
  • electronicHorizon:didReceivelinkAdded:link: - Notifies which link was just added to an NMAEHPathTree. Note that the main NMAEHPathTree does not contain a parent while an NMAEHPathTree may or may not contain children, no matter how deep it is nested in the hierarchy.
  • electronicHorizon:didReceiveLinkRemoved:link: - Called as soon as a link was removed from a parent NMAEHPathTree. Note that it is not guaranteed to be called for every link that was once added to a path tree.
  • electronicHorizon:didReceivePathAdded: - A new NMAEHPathTree is added to the existing tree. Since each NMAEHPathTree contains parents and children, you could start to traverse the road network from here although only electronicHorizon:didReceiveNewPosition: is guaranteed to provide the full path.
  • electronicHorizon:didReceivePathRemoved: - Called before electronicHorizon:didReceiveNewPosition: as soon as an NMAEHPathTree was removed. Usually this is a good place to remove any map objects from the map that belong to this NMAEHPathTree.
  • electronicHorizon:didReceiveChildDetached:child: - A path was detached from a parent because the user has left the main path but is still travelling 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 NMAPositioningManager, otherwise no events are delivered. Please note that electronicHorizon:didReceiveNewPosition: 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:

- (void)electronicHorizon:(nonnull NMAElectronicHorizon *)electronicHorizon
  didReceiveNewPosition:(nonnull NMAEHPosition *)position {
  NMAEHPathTree* pathTree = position.pathTree;
  if (pathTree == nil) {
    // we seem to be off-road

An NMAEHPathTree represents the road network ahead and by default also behind. It contains nested NMAEHPathTrees as children and parents but only the root path does not contain a parent NMAEHPathTree. This can be used to find out if an NMAEHPathTree represents the root path. The path belonging to the Position is always the main path on which the user is currently travelling. 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.

NMAEHPathTree* parentPathTree = pathTree.parent;
if (parentPathTree == nil) {
   // pathTree is the main path and the root path
} else {
   // pathTree is the main path, but not the root path

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

Predicting the Most Probable Path (MPP)

Without giving a route to follow, Electronic Horizon predicts the most probable path (which acts as root 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:

- (void)electronicHorizon:(nonnull NMAElectronicHorizon *)electronicHorizon
  didReceiveNewPosition:(nonnull NMAEHPosition *)position {
    NMAEHPathTree* pathTree = position.pathTree;
    if (pathTree == nil) {
      // off-road

    NMAEHPathTree* parentPathTree = pathTree.parent;
    if (parentPathTree == nil) {
       // pathTree is the main path

    // Main path is expected to have a probability of 1 (= 100%)
    float probability = pathTree.probability;
    NSLog(@"Probability of main path: %f", probability);

    for (NMAEHPathTree* childPathTree in pathTree.children) {
      NSLog(@"Probability of side path: %f", childPathTree.probability);

If a path tree 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.

Setting 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:

NMARoute* myRoute = [self getRoute];
[self.electronicHorizon setRoute: myRoute];

There is no option to unset a route as setting a nil route would lead to an NSError. 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 NMATransportModeCar and NMATransportModeTruck are supported. Other types throw an NSError.

Customize the Electronic Horizon Ahead and Behind

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

int trailingDistanceInMeters = 200;
[self.electronicHorizon setTrailingDistanceInCentimeters: trailingDistanceInMeters * 100];

int mainPathDistanceInMeters = 1000;
int sidePathDistanceInMeters = 500;
NSArray *distances = [NSArray arrayWithObjects:
  [NSNumber numberWithInt: mainPathDistanceInMeters * 100],
  [NSNumber numberWithInt: sidePathDistanceInMeters * 100], nil];
[self.electronicHorizon setLookAheadDistancesInCentimeters: distances];

Old links and path tree 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 path tree 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 starts from the vehicle 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 path tree 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 electronicHorizon:didReceiveNewPosition: callback:

- (void)electronicHorizon:(nonnull NMAElectronicHorizon *)electronicHorizon
  didReceiveNewPosition:(nonnull NMAEHPosition *)position {
    NMAEHPathTree* pathTree = position.pathTree;
    if (pathTree != nil && [pathTree isEqual: self.previousPathTree]) {
      // same PathTree object, we ignore any added paths
      // and wait for a newly created PathTree

    self.previousPathTree = pathTree;

    if (pathTree != nil) {
      [self showAllLinksInPathTree: pathTree];
    } else {
      // offroad
Note: An implementation may want to handle an added path as soon as the existing NMAEHPathTree object is extended. In this example we are only interested in a newly created path tree object to visualize the full path tree. Keep in mind that a user is usually travelling 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 path tree instead of being reset as part of a full tree reset (see electronicHorizonDidReceiveTreeReset:).

Once you can be sure to have a valid path tree, 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:

- (void) showAllLinksInPathTree: (NMAEHPathTree *) mainPathTree {
  int zIndex = 0;
  [self addLinksToHereMap: mainPathTree.links
          withColor: [UIColor redColor]
           andIndex: zIndex];

  // comment out the below to show only main path
  for (NMAEHPathTree* firstLevelSidePath in mainPathTree.children) {
    [self addLinksToHereMap: firstLevelSidePath.links
            withColor: [UIColor blueColor]
             andIndex: ++zIndex];

    for (NMAEHPathTree* secondLevelSidePath in firstLevelSidePath.children) {
      [self addLinksToHereMap: secondLevelSidePath.links
              withColor: [UIColor greenColor]
               andIndex: ++zIndex];
      // and so on ...

- (void) addLinksToHereMap: (NMAEHLinkRange *) links
         withColor: (UIColor *) linkColor
          andIndex: (int) zIndex {
  for (NMAEHLink* link in links) {
    NMAGeoPolyline* geoPolyline = [self.mapAccessor getLinkPolyline: link];
    if (geoPolyline) {
      NMAMapPolyline* mapPolyline = [[NMAMapPolyline alloc] initWithPolyline: geoPolyline];
      [mapPolyline setLineColor: linkColor];
      [mapPolyline setLineWidth: 10];
      [mapPolyline setZIndex: zIndex];
      [self.mapView addMapObject: mapPolyline];

By accessing an NMAGeoPolyline you can render the path as a map object onto the map as shown above. The path geometry of each link is defined by an NMAGeoPolyline you should extract using an NMAEHMapAccessor. Since the NMAGeoPolyline is not part of the link itself, it must be extracted from the map data. Therefore it is mandatory to check for nil to ensure that the needed map data is available.

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 path tree 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:

- (bool) isBackTurn: (NMAEHPathTree *) childPathTree {
  NMAEHPathTree* parentPath = childPathTree.parent;
  if (!parentPath) {
    return false;

  long childPathTreeOffset = [childPathTree offsetCentimeters];
  for (NMAEHLink* parentLink in parentPath.links) {
    if ([parentLink endOffsetCentimeters] == childPathTreeOffset) {
      NMAEHLink* firstInChild = childPathTree.links.nextObject;
      return == &&
        firstInChild.direction != parentLink.direction;
  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 path tree 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 NMAEHPathTree you receive with electronicHorizon:didReceiveNewPosition: 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 an NMAEHMapAccessor. Some attributes like slope data are directly accessible while NMAEHMetaData (such as side of driving or unit system) and NMAEHLinkInformation (such as form of route) hold multiple attributes. All attributes are valid for the entire link.

Below is an example on how to extract speed limits from NMAEHLinkInformation:

- (void)logSpeedLimits: (NMAEHPathTree *)pathTree {
  NMAEHMapAccessor* mapAccessor = self.electronicHorizon.mapAccessor;
  for (NMAEHLink* link in pathTree.links) {
    NMAEHLinkInformation* linkInformation = [mapAccessor getLinkInformation: link];
    if (linkInformation) {
      double speedLimitMetersPerSecond = linkInformation.speedLimitMetersPerSecond;
      NSLog(@"Speed limit on path: %ld Km", lroundl(speedLimitMetersPerSecond * 3.6));
    } else {
       NSLog(@"Map data not available");

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 NMAEHLinkInformation must be retrieved directly from NMAEHMapAccessor. Below you can find an example on how this could be implemented for road slope data:

double lengthInMeters;
NMAEHLinkInformation* linkInformation = [self.mapAccessor getLinkInformation: link];
if (linkInformation) {
  lengthInMeters = linkInformation.lengthMeters;
} else {
  NSLog(@"Map data not available");

NSArray<NMAEHSlopeDataPoint *>* slopeDataPointList = [self.mapAccessor getSlopeDataPoints: link];
if (!slopeDataPointList) {
  NSLog(@"Map data not available");

for (NMAEHSlopeDataPoint* slopeDataPoint in slopeDataPointList) {
  double normalizedPosition = slopeDataPoint.relativePositionOnLink;
  double positionOnLinkInMeters = normalizedPosition * lengthInMeters;
  double percent = slopeDataPoint.slopePercent;
  NSLog(@"Slope data position: %f", positionOnLinkInMeters);

  // if percent is greater/smaller than +/- 100% => Infinity
  if (percent == +INFINITY) {
    NSLog(@"Ascending slope > 45°");
  } else if (percent == -INFINITY) {
    NSLog(@"Downward slope > 45°");
  } else {
    if (percent > 0) {
      NSLog(@"Ascending slope: %f", percent);
    } else {
      NSLog(@"Downward slope: %f", percent);

While the raw slope data is extracted from [self.mapAccessor getSlopeDataPoints: link], you may also want to retrieve additional data from NMAEHLinkInformation such as the total length in metres of the current link. An NMAEHSlopeDataPoint holds only the normalized position of the link (for example, 0.0 = Start, 0.5 = Middle, 1.0 = End). In order to find the position of the NMAEHSlopeDataPoint relative to the beginning of the link, we should multiply its position by 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 NMAGeoPolyline of the link (see above).

Retrieving 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 NMAAdasisV2Engine which consumes that configuration.

To receive messages, the delegate class must conform to the NMAAdasisV2EngineDelegate protocol which provides adasisV2Engine:didReceiveAdasisMessage: callback.

- (id)init {
   NMAAdasisV2MessageConfiguration* adasisV2MessageConfiguration =

   self.adasisV2Engine = [[NMAAdasisV2Engine alloc] initWithConfiguration:adasisV2MessageConfiguration];
   self.adasisV2Engine.delegate = self;

 - (void)adasisV2Engine:(nonnull NMAAdasisV2Engine *)adasisV2Engine
didReceiveAdasisMessage:(nonnull NSData *)message {
   // 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.
   NSLog(@"ADASIS message received. Length: %lu", (unsigned long)message.length);

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

NMAAdasisV2MessageConfiguration* adasisV2MessageConfiguration =

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 its parts containing data (such as slopes) individually like shown below for SEGMENT message type:

if (adasisV2MessageConfiguration.segmentEnabled) {
  adasisV2MessageConfiguration.segmentEnabled = false;

You can run a single NMAAdasisV2Engine instance along with an NMAElectronicHorizion instance but you have to make sure to call [self.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 NMAElectronicHorizion instance. Therefore it is required to start NMAPositioningManager 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. In order to establish a communication to a client, please 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.