HERE iOS SDK Developer's Guide

Platform Data Extension

Platform Data Extension (PDE) provides the ability to easily access the Platform Data Extension API from the HERE iOS SDK. You can use this extension to access a wide range of data that can be later used for different use cases. Some examples include displaying road elevation, slopes, and traffic signs. For more information about use cases and the types of data that can be accessed through PDE, check the Platform Data Extension API Developer's Guide .

PDE Thematic Layers

PDE divides map content across many thematic layers. Each thematic data layer serves a specific use case and only contains the data required for it, such as road elevation. To use PDE, you need to first decide on the required data for your app and select the PDE thematic layers accordingly. The available thematic layers can be found via the PDE Layers API. You can then check the targeted thematic layer via the individual Layer API. Before starting to use PDE in your app, you need to select the correct thematic layers, and then decide on what data to use and how to use it. This is a crucial step.

Note: It is common for routes to start on smaller roads, climb to bigger roads, stay on motorways for the main part, and finally steps down to smaller roads again when approaching the destination. Since retrieving all information about smaller roads along the entire route requires a large amount of data, road link-related thematic layers are actually split into five layers each, corresponding to the functional road classes in the HERE map. Functional Class 1 roads are generally motorways, while Functional Class 5 roads are small roads that are only used near a destination. To use these layers, you need to specify the tile layer by appending the functional class suffix "_FCx", where x is a number from 1 to 5. For example, ROAD_GEOM_FC1. If a layer isn't related to road links, such as the PSTLCB_GEN layer, you don't need to append the "_FCx" suffix and specify the tile layer.

PDE Classes

Class Description
NMAPlatformDataRequest Creates a PDE data request with the specified layers and NMAGeoBoundingBox object.
NMAPlatformDataResult After a PDE data request, the result is returned as an object of this type. Provides an extract method to convert the result into an NSDictionary object.
NMAPlatformDataItemCollection Array of NMAPlatformDataItem objects. For example, assuming the result is an NMAPlatformDataResult type object, the NMAPlatformDataItemCollection for the ROAD_GEOM_FC1 layer data can be accessed with result[@"ROAD_GEOM_FC1"]. This class also provides an extract method to convert the collection into an NSArray object.
NMAPlatformDataItem After a PDE data request, each layer data is returned with objects of this class. For example, assuming the item is an NMAPlatformDataItem object containing the ROAD_GEOM_FC1 layer data, the name property can be accessed with item[@"NAME"]. If the specified property isn't found, nil is returned. Note that several properties shortcuts are available like item.linkId versus item[@LINK_ID"]. This class also provides extract method to convert the PDE data into an NSDictionary object.

Example: Requesting the PDE Data

The example below shows how a feature is implemented using the PDE data. The goal is to colorize each road segment according to its average height. For this feature, you need the PDE data from the ROAD_GEOM_FC1 and BASIC_HEIGHT_FC1 layers. As of now, the only way to request the PDE data requires the layers specified with an NMAGeoBoundingBox.

Note: The ROAD_GEOM_FC[number] and BASIC_HEIGHT_FC[number] layers are also referred to as "tile" layers, since their data is split into multiple tiles. Due to server limitations, up to 15 tiles can be requested at a time. PDE also supports non-tile layers.

To use the data from both layers, you need to join the two thematic layers by using the LINK_ID property. This is demonstrated in the next section.

The following are sample results from the Layer request.

ROAD_GEOM_FC1 Layer API result:

{
  "description": "Ungeneralized road, ferry and rail ferry geometry (polylines).<br/>If a road link crosses a tile boundary, it will be written into each of the tiles, each including the full link geometry. This simplifies use cases other than pure display of all geometry within a rectangle.",
  "attributes": {
  "LINK_ID": "Permanent link ID. Positive 64 bit Integer that globally identifies the road, carto or buildin footprint link, also across map releases. Link IDs are never reused.",
  "LONG_HAUL": "This link or polygon or POI is of major importance. It should be displayed at high zoom lavels, and it should be included for routing in/through regions where no detailed routing is supported.",
  "NAME": "A name of this road line. Roads can have multiple names, in the same or multiple languages. This field contains any of those.",
  "NAMES": "List of all names for this object, in all languages [...]",
  "TUNNEL": "Is this navigable link or railroad a tunnel?",
  "BRIDGE": "Is this navigable link or railroad a bridge?",
  "LAT": "Latitude coordinates [10^-5 degree WGS84] along the polyline. ",
  "LON": "Longitude coordinates [10^-5 degree WGS84] along the polyline. ",
  "ZLEVEL": "(-4 ... 11) indicates the height of the point relative to another point on a grade separated crossing with any other line. Comma separated. If z-level is null then the value '0' is left out."
  },
  "referencedStaticContents": [],
  "tileRequestsLevel": 9,
  "tileX": 499,
  "tileY": 403,
  "isStaticContent": false
}

BASIC_HEIGHT_FC1 Layer API result:

{
  "description": "Link height values computed from a Digital Terrain Model, cleaned up for continuity along links, bridges and tunnels. Less accurate than ADAS link height values, but full coverage and sufficient for certain use cases.",
  "attributes": {
  "LINK_ID": "Permanent link ID. Positive 64 bit Integer that globally identifies the road, carto or buildin footprint link, also across map releases. Link IDs are never reused.",
  "DTM_MIN_HEIGHT": "The minimum height [cm above WGS84 ellipsoid] encountered along the link.",
  "DTM_MAX_HEIGHT": "The maximum height [cm above WGS84 ellipsoid] encountered along the link.",
  "DTM_AVG_HEIGHT": "The average height [cm above WGS84 ellipsoid] along the link.",
  "DTM_REF_ZCOORD": "Height [cm above WGS84 ellipsoid] at the reference node of the link.",
  "DTM_NONREF_ZCOORD": "Height [cm above WGS84 ellipsoid] at the non-reference node of the link."
  },
  "referencedStaticContents": [],
  "tileRequestsLevel": 9,
  "tileX": 496,
  "tileY": 358,
  "isStaticContent": false
}

To begin using the PDE layers, create a request object of NMAPlatformDataRequest type:

NSSet<NSString *> *layers = [NSSet setWithObjects:@"ROAD_GEOM_FC1", @"BASIC_HEIGHT_FC1", nil];
NMAGeoBoundingBox *box = self.mapView.boundingBox;
NMAPlatformDataRequest *request = [[NMAPlatformDataRequest alloc] initWithLayers:layers geoBoundingBox:box];

If the request is found to be invalid, nil is returned. You can run the request with a block or listener:

// Is the request valid?
if (request) {
  [request startWithBlock:^(NMAPlatformDataRequest *request, NMAPlatformDataResult *result, NSError *error) {

    if (error) {
      NSLog(@"Error occurred!\n"
           "Code: %ld\n"
           "Description: %@\n"
           "Failure reason: %@\n",
          (long)error.code,
          error.localizedDescription,
          error.localizedFailureReason];
    }
    else {
      // Retrieved the result successfully, now process it
      RoadElevationProcessor *processor =
        [[RoadElevationProcessor alloc] initWithResult:result
                         andMapContainer:self.mapContainer];

      [processor process:^void (BOOL result, NSString *msg) {
        if (result) {
          [self addElevationLegend];
        } else {
          NSLog(@"Elevation data processing failed!");
        }

        if (msg) {
          NSLog(msg);
        }
      }];
    }
  }];
}
else {
  NSLog(@"Invalid request!");
};

Example: Processing the PDE Data

After the data result is successfully retrieved, you need to process the data. In the example above, you have asked for ROAD_GEOM_FC1 and BASIC_HEIGHT_FC1 data, restricted with the map view's bounding box. For tutorial purposes, we then implemented a sample data processor class, PlatformDataProcessor, which provides methods to join the layers and can be extended for consuming the joined data. In this example, this is required to implement the colorize feature.

The following is the class declaration for PlatformDataProcessor:

/**
 * Defines the code block that returns a string given a data item object.
 * It is used to index data item objects for quickly joining the data.
 */
typedef NSString* (^NMAPlatformDataProcessorIndexerBlock)(NMAPlatformDataItem*);

/**
 * Defines the code block that returns a string array given a data item
 * object. It is used to index data item objects for quickly joining
 * the data.
 */
typedef NSArray<NSString*>* (^NMAPlatformDataProcessorMultiIndexerBlock)(NMAPlatformDataItem*);

/**
 * Defines the code block that runs after join a operation.
 * The first argument sets the result and the other one sets the message
 * if available.
 */
typedef void (^NMAPlatformDataProcessorResultBlock)(BOOL, NSString*);

/**
 * \brief This class is for joining the PDE data coming from different
 *    layers using the passed indexers.
 */
@interface PlatformDataProcessor : NSObject

/**
 * This method joins two different layers data using the indexers and returns
 * a dictionary of items.
 *
 * \param mainLayer The layer data that will joined with the other layer data.
 *
 * \param mainIndexer The indexer for quickly joining the main layer data.
 *
 * \param otherLayer The other data that will joined with the main layer data.
 *
 * \param otherIndexer The indexer for quickly joining the other layer data.
 *
 * \return The dictionary of items that are joined where the key is the PDE data
 *     coming from the main layer data and the value is the PDE data coming
 *     from the other layer data.
 */
- (NSDictionary<NMAPlatformDataItem*, NMAPlatformDataItem*>*)
  joinLayer:(NMAPlatformDataItemCollection *)mainLayer
  usingIndexer:(NMAPlatformDataProcessorIndexerBlock)mainIndexer
  withOtherLayer:(NMAPlatformDataItemCollection *)otherLayer
  usingIndexer:(NMAPlatformDataProcessorIndexerBlock)otherIndexer;

/**
 * This method joins two different layers data using the indexers and returns
 * a dictionary of items.
 *
 * \param mainLayer The layer data that will joined with the other layer data.
 *
 * \param mainMultiIndexer The indexer for quickly joining the main layer data.
 *
 * \param otherLayer The other data that will joined with the main layer data.
 *
 * \param otherMultiIndexer The indexer for quickly joining the other layer data.
 *
 * \return The dictionary of items that are joined where the key is the PDE data
 *     coming from the main layer data and the value is the array of PDE data
 *     coming from the other layer data.
 */
- (NSDictionary<NMAPlatformDataItem*, NSArray<NMAPlatformDataItem*>*>*)
  joinLayer:(NMAPlatformDataItemCollection *)mainLayer
  usingMultiIndexer:(NMAPlatformDataProcessorMultiIndexerBlock)mainMultiIndexer
  withOtherLayer:(NMAPlatformDataItemCollection *)otherLayer
  usingMultiIndexer:(NMAPlatformDataProcessorMultiIndexerBlock)otherMultiIndexer;

/**
 * This method processes the data joined.
 *
 * \note The derived classes must implement this method.
 *
 * \param block The block that runs after the join operation. Note that its first argument brings
 *        the result and the other one brings a message if available.
 */
- (void)process:(NMAPlatformDataProcessorResultBlock)block;

Note that [PlatformDataProcessor joinLayer:usingIndexer:withOtherLayer:usingIndexer] method is for inner joining the thematic layers with properties where more than one data item is possible. For example, the TRAFFIC_SIGN_FCx thematic layer has the LINK_IDS property where as much as two LINK_IDs are contained.

The following is the PlatformDataProcessor class implementation:

@interface PlatformDataProcessor ()
{
}

- (NSDictionary<NSString*, NMAPlatformDataItem*>*)
  map:(NMAPlatformDataItemCollection *)collection
  withIndexer:(NMAPlatformDataProcessorIndexerBlock)indexer;

- (NSDictionary<NSString*, NSArray<NMAPlatformDataItem*>*>*)
  multiMap:(NMAPlatformDataItemCollection *)collection
  withMultiIndexer:(NMAPlatformDataProcessorMultiIndexerBlock)indexer;

@end

@implementation PlatformDataProcessor

#pragma mark - Public methods

- (NSDictionary<NMAPlatformDataItem*, NMAPlatformDataItem*>*)
  joinLayer:(NMAPlatformDataItemCollection *)mainLayer
  usingIndexer:(NMAPlatformDataProcessorIndexerBlock)mainIndexer
  withOtherLayer:(NMAPlatformDataItemCollection *)otherLayer
  usingIndexer:(NMAPlatformDataProcessorIndexerBlock)otherIndexer
{
  NSDictionary<NSString*, NMAPlatformDataItem*>* mappedItems =
    [self map:otherLayer withIndexer:otherIndexer];
  NSMutableDictionary<NMAPlatformDataItem*, NMAPlatformDataItem*>* dictionary =
    [[NSMutableDictionary alloc] init];

  for (NMAPlatformDataItem *item in mainLayer) {
    NSString *key = mainIndexer(item);
    NMAPlatformDataItem *matchedItem = mappedItems[key];

    if (matchedItem) {
      dictionary[item] = matchedItem;
    }
  }

  return dictionary;
}

- (NSDictionary<NMAPlatformDataItem*, NSArray<NMAPlatformDataItem*>*>*)
  joinLayer:(NMAPlatformDataItemCollection *)mainLayer
  usingMultiIndexer:(NMAPlatformDataProcessorMultiIndexerBlock)mainMultiIndexer
  withOtherLayer:(NMAPlatformDataItemCollection *)otherLayer
  usingMultiIndexer:(NMAPlatformDataProcessorMultiIndexerBlock)otherMultiIndexer
{
  NSDictionary<NSString*, NSArray<NMAPlatformDataItem*>*>* mappedItems =
    [self multiMap:otherLayer withMultiIndexer:otherMultiIndexer];
  NSMutableDictionary<NMAPlatformDataItem*, NSMutableArray<NMAPlatformDataItem*>*>* dictionary =
    [[NSMutableDictionary alloc] init];

  for (NMAPlatformDataItem *item in mainLayer) {
    NSArray<NSString*> *array = mainMultiIndexer(item);

    for (NSString *key in array) {
      NSArray<NMAPlatformDataItem*> *matchedItems = mappedItems[key];

      if (matchedItems) {
        NSMutableArray<NMAPlatformDataItem*> *entry = dictionary[item];

        if (!entry) {
          entry = [[NSMutableArray alloc] init];
          dictionary[item] = entry;
        }

        [entry addObjectsFromArray:matchedItems];
      }
    }
  }

  return dictionary;
}

- (void)process:(NMAPlatformDataProcessorResultBlock)block
{
  NSLog(@"Derived classes must implement [PlatformDataProcessor process:] method!");
  abort();
}

#pragma mark - Private methods

- (NSDictionary<NSString*, NMAPlatformDataItem*>*)
  map:(NMAPlatformDataItemCollection *)collection
  withIndexer:(NMAPlatformDataProcessorIndexerBlock)indexer
{
  NSMutableDictionary<NSString*, NMAPlatformDataItem*>* dictionary =
    [[NSMutableDictionary alloc] init];

  for (NMAPlatformDataItem* item in collection) {
    NSString* key = indexer(item);

    dictionary[key] = item;
  }

  return dictionary;
}

- (NSDictionary<NSString*, NSArray<NMAPlatformDataItem*>*>*)
  multiMap:(NMAPlatformDataItemCollection *)collection
  withMultiIndexer:(NMAPlatformDataProcessorMultiIndexerBlock)indexer
{
  NSMutableDictionary<NSString*, NSMutableArray<NMAPlatformDataItem*>*>* dictionary =
    [[NSMutableDictionary alloc] init];

  for (NMAPlatformDataItem* item in collection) {
    NSArray<NSString*>* array = indexer(item);

    for (NSString* key in array) {
      if (!dictionary[key]) {
        dictionary[key] = [[NSMutableArray  alloc] init];
      }

      [dictionary[key] addObject:item];
    }
  }

  return dictionary;
}

@end

Next, create RoadElevationProcessor. The RoadElevationProcessor class extends the PlatformDataProcessor for coloring the road segments according to their average height. This class needs the result and a dedicated map container intended for the NMAMapPolyline objects. Note that the base class PlatformDataProcessor performs the inner joining of the thematic layer data depending on the indexer blocks provided. The RoadElevationProcessor class utilizes the joined data to implement the intended feature.

Here's the declaration for the RoadElevationProcessor class:

/**
 * \brief Extends the PlatformDataProcessor class for the road elevation
 *    handling.
 */
@interface RoadElevationProcessor : PlatformDataProcessor

/**
 * Creates a road elevation processor object.
 *
 * \param result The PDE road elevation data result to be processed.
 *
 * \param mapContainer The polylines that will be colored according to the average road
 *           segments will be inserted to this map container.
 *
 * \return If the parameters are valid, a RoadElevationProcessor object configured with
 *     the parameters and nil otherwise.
 */
- (instancetype)initWithResult:(NMAPlatformDataResult *)result
         andMapContainer:(NMAMapContainer *)mapContainer;

@end;

The main method for the RoadElevationProcessor class is the process method below:

- (void)process:(NMAPlatformDataProcessorResultBlock)block
{
  BOOL result = FALSE;
  NSString *msg;
  NSDictionary<NMAPlatformDataItem*, NMAPlatformDataItem*>* joinedItems =
    [self joinLayer:_result[@"ROAD_GEOM_FC1"]
      usingIndexer:^NSString* (NMAPlatformDataItem *item) {
        return item.linkId;
      }
      withOtherLayer:_result[@"BASIC_HEIGHT_FC1"]
      usingIndexer:^NSString* (NMAPlatformDataItem *item) {
        return item.linkId;
      }];

  // Any joined data?
  if (joinedItems && joinedItems.count) {
    // Set the message
    msg = [NSString stringWithFormat:@"Joined elevation items count = %ld\n",
      (unsigned long)joinedItems.count];

    // One-by-one check the joined data
    for (NMAPlatformDataItem *roadGeometry in joinedItems) {
      // Filter out the road geometries having less than 2 coordinates
      // as they show nothing on the map
      if (roadGeometry.coordinates.count > 1) {
        NMAPlatformDataItem *height = joinedItems[roadGeometry];
        NMAMapPolyline *polyline =
          [NMAMapPolyline mapPolylineWithVertices:roadGeometry.coordinates];

        polyline.lineColor = [self getColorDependingOnHeight:height.averageHeightCm];
        polyline.lineWidth = 10;

        [_mapContainer addMapObject:polyline];
      }
    }

    result = TRUE;
  }

  // Done: pass the result along with the msg
  block(result, msg);
}

The first thing this method does is joining the data coming from two different thematic layers with the "indexer" blocks. Here, the data is joined via the LINK_ID's. Note that the linkId property is available as a shortcut property in the NMAPlatformDataItem class. After joining the data, each ROAD_GEOM_FC1 data item is inner joined with the relevant BASIC_HEIGHT_FC1 data item, and the joined data is returned as a dictionary where the actual type is NSDictionary<NMAPlatformDataItem*, NMAPlatformDataItem*>. Each key-value pairs provides the necessary data for the feature: the key of NMAPlatformDataItem type representing the ROAD_GEOM_FC1 thematic layer data provides the road geometry, such as coordinates of the road segment, and the value of NMAPlatformDataItem type, representing the BASIC_HEIGHT_FC1 thematic layer data, provides the average height of that road segment. The joined data provides all the data required for implementing the intended feature.

Joined data using LINK_ID should look like the following:

{
  BRIDGE = N;
  LAT = "5246124,2";
  "LINK_ID" = 936938339;
  LON = "1342560,24";
  "LONG_HAUL" = Y;
  NAME = A100;
  NAMES = "GERBNTunnel BritzGERY\"tU|n@l \"brItsGERBNA100GERN\"?a: \"hUn|d6t;GERN\"?aU|to:|ba:n ?aIn|\"hUn|d6t;GERY\"?a: ?aIn|\"hUn|d6t;GERN\"?aU|to:|ba:n \"hUn|d6tGERBNStadtring BerlinGERY\"Stat|rIN bEr|\"li:n";
  TUNNEL = Y;
  ZLEVEL = ",";
} = {
  "DTM_AVG_HEIGHT" = 8500;
  "DTM_MAX_HEIGHT" = 8693;
  "DTM_MIN_HEIGHT" = 8099;
  "DTM_NONREF_ZCOORD" = 8500;
  "DTM_REF_ZCOORD" = 8500;
  "LINK_ID" = 936938339;
}

Note that the key-value pair has the same LINK_ID.