SDK for iOS Developer's Guide

Custom Raster Tiles

You can use SDK for iOS to enhance maps with the custom raster tiles API — NMAMapTileLayer.

Custom raster tiles are tile images that you can add to a map for enhancing the map with extra information over a large geographical area. If the application is set to display custom raster tiles, then users see them whenever they view a designated geographical area at a specified zoom level or range of zoom levels.

You can provide tile images in two ways:
  1. Store custom raster tile images on a remote server and return URLs via NMAMapTileLayerDataSource mapTileLayer:urlForTileAtX:y:zoomLevel: protocol method.
  2. Provide raw bitmap data using NMAMapTileLayerDataSource mapTileLayer:requestDataForTileAtX:y:zoomLevel:tileRequest: protocol method.
Note: Before enabling raster tiles ensure that the extruded buildings feature is disabled as the appearance of both features does not go well together.

Map Raster Tile Example on GitHub

You can find an example that demonstrates this feature at https://github.com/heremaps/ (Obj-C) and https://github.com/heremaps/ (Swift).

Dividing a Map and Using Tile Coordinates

NMAMapTileLayer uses a scheme that divides the world map into tiles specified by x, y, and zoom level coordinates. This coordinate system is used by the NMAMapTileLayerDataSource protocol when it requests tiles.

At each zoom level it is expected that the world map is rendered on (2 zoomlevel )2 tiles:
  • at level 0: 1 x 1 = 1 tile
  • at level 1: 2 x 2 = 4 tiles
  • at level 2: 4 x 4 = 16 tiles
  • at level 3: 8 x 8 = 64 tiles
  • at level 4: 16 x 16 = 256 tiles
  • continuing on until zoom level 20

For example, at zoom level 2 the world map would be divided up as follows:

Figure 1. World Map at Zoom Level 2

The x and y parameters indicate which tile is being requested for the given zoom level.

You need to provide enough tile images to cover all the zoom levels you are supporting within a geographical area. You can restrict custom tile rendering to a specific NMAGeoBoundingBox using boundingBox property of NMAMapTileLayer. You can restrict the zoom level using showAtZoomLevel: and related methods.

Supplying Tiles from a Web Server

The steps for providing custom tiles from a web server to a map view are as follows:
  1. Host the appropriate number of tiles on a server according to the zoom levels and NMAGeoBoundingBox you are supporting. The tiles must be in either PNG or JPG format and should be sized at 256 x 256 pixels (this is the default size but can be changed).
  2. Create an object that derives from NMAMapTileLayerDataSource and implement mapTileLayer:urlForTileAtX:y:zoomLevel:method to return a URL pointing to the specified tile on your server.
  3. Create an NMAMapTileLayer object and set its properties to correspond to the tile data source server. At least, set boundingBox and zoomLevel properties to reflect the tiles hosted on your server. Set dataSource property.
  4. Optionally, customize the appearance of the tiles within the map view by setting properties such as fadingEnabled.
  5. Add the NMAMapTileLayer object to the NMAMapView by calling addMapTileLayer: method.

The following code snippet shows a class that renders the Queen Elizabeth Olympic Park in London. The park is displayed as a tile layer that is added to an NMAMapView, and the raster tiles are served from HERE server. To use this class, call [OlympicParkTileLayer addOlympicParkTileLayerToMapView:myMapView].


@interface OlympicParkTileLayer : NMAMapTileLayer <NMAMapTileLayerDataSource>
@end

@implementation OlympicParkTileLayer

+(void)addOlympicParkTileLayerToMapView:(NMAMapView*)mapView
{
  OlympicParkTileLayer *tileLayer = [OlympicParkTileLayer new];
  [mapView addMapTileLayer:tileLayer];
  [mapView setGeoCenter:tileLayer.boundingBox.center
      zoomLevel:14.0
      orientation:NMAMapViewPreserveValue
      tilt:NMAMapViewPreserveValue
      withAnimation:NMAMapAnimationNone ];
}

-(id)init
{
  if (self = [super init]) {
    // Set the data source
    self.dataSource = self;

    // Set the bitmap format properties (must be compatible with pngs hosted on the
    // server)
    self.pixelFormat = NMAPixelFormatRGBA;
    self.transparent = NO;

    // Limit the tiles to the bounding box supported by the server
    NMAGeoBoundingBox *olympicParkBoundingBox =
    [NMAGeoBoundingBox geoBoundingBoxWithTopLeft:
      [NMAGeoCoordinates geoCoordinatesWithLatitude:51.557000 longitude:-0.042772]
      bottomRight:
      [NMAGeoCoordinates geoCoordinatesWithLatitude:51.525941 longitude: 0.028296]];
    self.boundingBox = olympicParkBoundingBox;

    // Customize the tile layer
    self.fadingEnabled = YES;
    // covers everything else on the map
    self.mapLayerType = NMAMapLayerTypeForeground;

    // Enable caching
    self.cacheTimeToLive = 60 * 60 * 24;  // 24 hours
    self.cacheSizeLimit = 1024 * 1024 * 64; // 64MB
    [self setCacheEnabled:YES withIdentifier:@"OlympicParkTileLayer"];
  }
  return self;
}

-(NSString *)mapTileLayer:(NMAMapTileLayer *)mapTileLayer
      urlForTileAtX:(NSUInteger)x
            y:(NSUInteger)y
        zoomLevel:(NSUInteger)zoomLevel
{
  // Return a URL for the specified tile
  // This tile source is hosted by HERE Global B.V. and may be removed at any time
  return [NSString stringWithFormat:
    @"http://api.maps.example.org/maptiles/olympic_park/normal.day/%d/%d/%d.png",
    zoomLevel,
    y,
    x ];
}

@end

Supplying Tiles as Bitmaps

You can choose to supply tiles as bitmaps if your app uses bundled static tiles, dynamically generated tiles, or if the server that you are using for tile images requires authentication. In the third case, since NMAMapTileLayer only uses simple HTTP GET requests to retrieve tile images, it is up to you to provide code that handles authentication and downloads the tiles. Once the tiles have been downloaded, you can use them as local bitmaps with the NMAMapTileLayer class.

The steps for providing custom tiles as local bitmaps to a map view are as follows:
  1. Create an object that derives from NMAMapTileLayerDataSource and implements mapTileLayer:requestDataForTileAtX:y:zoomLevel:tileRequest: method. Retain and use NMAMapTileRequest parameter to supply the requested bitmap data.
  2. Create an NMAMapTileLayer object and set its properties to correspond to the tile data source. Set dataSource property.
  3. Optionally, customize the appearance of the tiles within the map view by setting properties such as fadingEnabled.
  4. Add the NMAMapTileLayer object to the NMAMapView by calling addMapTileLayer: method.

The following is an example of a class that renders red and green tiles on an NMAMapView using a tile layer. It can be used by calling [SimpleBitmapTileLayer addSimpleBitmapTileLayerToMapView].


@interface SimpleBitmapTileLayer : NMAMapTileLayer <NMAMapTileLayerDataSource>
{
  char *_redBitmap;
  char *_greenBitmap;
}
@end

@implementation SimpleBitmapTileLayer

+(void)addSimpleBitmapTileLayerToMapView:(NMAMapView*)mapView
{
  [mapView addMapTileLayer:[SimpleBitmapTileLayer new]];
}

-(id)init
{
  if (self = [super init]) {

    // Set the data source
    self.dataSource = self;

    // Set the bitmap properties (must match what is returned in 
    // requestDataForTileAtX:)
    self.pixelFormat = NMAPixelFormatRGBA;
    self.transparent = NO;

    // The tiles will replace the map background
    self.mapLayerType = NMAMapLayerTypeArea;

    // We want the tiles to span the globe so don't limit with a bounding box or 
    // zoom level.

    // We don't want to cache these tiles to disk, as in this simple case
    // it's more efficient to keep bitmaps in memory.

    // Create a red bitmap
    _redBitmap = (char *)malloc(256*256*4);
    for (int i=0; i<256*256*4; i+=4) {
      _redBitmap[i] = 0xFF;
      _redBitmap[i+1] = 0x00;
      _redBitmap[i+2] = 0x00;
      _redBitmap[i+3] = 0xFF;
    }

    // Create a green bitmap
    _greenBitmap = (char *)malloc(256*256*4);
    for (int i=0; i<256*256*4; i+=4) {
      _greenBitmap[i] = 0x00;
      _greenBitmap[i+1] = 0xFF;
      _greenBitmap[i+2] = 0x00;
      _greenBitmap[i+3] = 0xFF;
    }
  }

  return self;
}

-(void)dealloc
{
  free(_redBitmap);
  free(_greenBitmap);
}

-(void)   mapTileLayer:(NMAMapTileLayer *)mapTileLayer
   requestDataForTileAtX:(NSUInteger)x
             y:(NSUInteger)y
         zoomLevel:(NSUInteger)zoomLevel
       tileRequest:(NMAMapTileRequest *)tileRequest
{
  // Dispatch to a global queue to demonstrate retaining the tile request and 
  // completing asynchronously. In this case it is not necessary but if obtaining 
  // the bitmap data was a lengthy operation we would not want to block the current 
  // (map rendering) thread.
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Use alternating red and green bitmaps to create checkerboard effect
    void *bitmap = ( (x%2==0 && y%2==1) || (x%2 ==1 && y%2==0) ) ? _redBitmap : 
      _greenBitmap;

    // Copy the bitmap into the bitmap buffer supplied by the tile request
    // Copy exactly tileRequest.bytesLength
    memcpy(tileRequest.bytesPtr, bitmap, tileRequest.bytesLength);

    // Complete the tile request AFTER the copy has completed
    tileRequest.status = NMAMapTileRequestStatusComplete;
  });
}

@end
There are a couple of important points to consider when using the NMAMapTileRequest object:
  1. The bitmap data must be in the format specified by NMAMapTileLayer pixelFormat property. The default is NMAPixelFormatRGBA.
  2. Copy the bitmap bytes before you complete the request and only copy the number of bytes specified by bytesLength to avoid memory corruption.
  3. Always complete the request precisely once with either NMAMapTileRequestStatusComplete or NMAMapTileRequestStatusFailed. Uncompleted requests occupy memory until they are completed.
  4. Do not block mapTileLayer:requestDataForTileAtX:y:zoomLevel:tileRequest: for an extended period of time as this impacts rendering performance.

Changing the Overlay Rendering Order

You can choose to customize the map layer that raster tiles are rendered by using mapLayerType property. For example, if you place your tiles at NMAMapLayerTypeForeground layer along with the NMAMapObjects you have created, you can use zIndex to control if the objects are rendered on top of, or beneath, the tiles.

Other Tile Customizations

NMAMapTileLayer supports properties to further customize the appearance of the tiles such as:
  • tile size
  • pixel format such as RGB or BGRA (to reduce memory usage)
  • whether tiles fade in as they are drawn
  • whether tiles from a different zoom level are rendered in place of a missing tile
  • whether the tiles support transparency

Caching Tiles

Tiles can be cached to the disk to improve performance and reduce data traffic in the URL fetching case.

When you enable caching, you must provide a cache identifier. This identifier must be unique for each NMAMapTileLayer used within your application. Since the cache persists across app sessions, it is important to use the same identifier across sessions (by defining a constant, for example).

You can optionally limit the cache size and time to live for each cached tile. The cache can be cleared at any time by calling [NMAMapTileLayer clearCache]. To be sure the cache is completely cleared, first remove the NMAMapTileLayer from the map view before calling [NMAMapTileLayer clearCache].

The following code enables disk caching with a 128MB maximum size and a tile time to live of 7 days:

NMAMapTileLayer *tileLayer = [[NMAMapTileLayer alloc]init];
tileLayer.dataSource = self; // Assuming self is a valid data source
[tileLayer setCacheEnabled:YES withIdentifier:@"MyUniqueTileCacheIdenfifier"];
tileLayer.cacheTimeToLive = 60 * 60 * 24 * 7; // 7 days
tileLayer.cacheSizeLimit = 1024 * 1024 * 128; // 128 MB
[mapView addMapTileLayer:tileLayer]; // NOTE: add to map view after setting tile properties
      

Performance Tips

  1. IMPORTANT: Set NMAMapTileLayer properties before adding the tile layer to the map view. Most properties ignore attempts to set them after being added to the view.
  2. Do not attempt to add a single NMAMapTileLayer instance to multiple maps.
  3. Ensure the properties you set on the tile layer match the data you are supplying via the NMAMapTileLayerDataSource protocol. For example, do not enable transparency unless your tile data supports it.
  4. Do not block NMAMapTileLayerDataSource methods for extended periods of time. For example, if it takes a while to generate tiles on the fly, move the processing to a separate GCD queue.
  5. Ensure all NMAMapTileRequest objects received via mapTileLayer:requestDataForTileAtX:y:zoomLevel:tileRequest: are completed to avoid memory leaks.
  6. Write directly to NMAMapTileRequest bytesPtr property to avoid memory copies.
  7. If requesting a specific tile is constantly failing, consider implementing mapTileLayer:hasTileAtX:y:zoomLevel: returning NO.
  8. Use the provided disk caching mechanism.