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.
- Store custom raster tile images on a remote server and return URLs via
NMAMapTileLayerDataSource mapTileLayer:urlForTileAtX:y:zoomLevel:
protocol method. - Provide raw bitmap data using
NMAMapTileLayerDataSource mapTileLayer:requestDataForTileAtX:y:zoomLevel:tileRequest:
protocol method.
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 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:

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
- 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). - Create an object that derives from
NMAMapTileLayerDataSource
and implementmapTileLayer:urlForTileAtX:y:zoomLevel:method
to return a URL pointing to the specified tile on your server. - Create an
NMAMapTileLayer
object and set its properties to correspond to the tile data source server. At least, setboundingBox
andzoomLevel
properties to reflect the tiles hosted on your server. SetdataSource
property. - Optionally, customize the appearance of the tiles within the map view by setting properties such as
fadingEnabled
. - Add the
NMAMapTileLayer
object to theNMAMapView
by callingaddMapTileLayer:
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.
- Create an object that derives from
NMAMapTileLayerDataSource
and implementsmapTileLayer:requestDataForTileAtX:y:zoomLevel:tileRequest:
method. Retain and useNMAMapTileRequest
parameter to supply the requested bitmap data. - Create an
NMAMapTileLayer
object and set its properties to correspond to the tile data source. SetdataSource
property. - Optionally, customize the appearance of the tiles within the map view by setting properties such as
fadingEnabled
. - Add the
NMAMapTileLayer
object to theNMAMapView
by callingaddMapTileLayer:
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
NMAMapTileRequest
object: - The bitmap data must be in the format specified by
NMAMapTileLayer pixelFormat
property. The default isNMAPixelFormatRGBA
. - Copy the bitmap bytes before you complete the request and only copy the number of bytes specified by
bytesLength
to avoid memory corruption. - Always complete the request precisely once with either
NMAMapTileRequestStatusComplete
orNMAMapTileRequestStatusFailed
. Uncompleted requests occupy memory until they are completed. - 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
- 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. - Do not attempt to add a single
NMAMapTileLayer
instance to multiple maps. - 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. - 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. - Ensure all
NMAMapTileRequest
objects received viamapTileLayer:requestDataForTileAtX:y:zoomLevel:tileRequest:
are completed to avoid memory leaks. - Write directly to
NMAMapTileRequest bytesPtr
property to avoid memory copies. - If requesting a specific tile is constantly failing, consider implementing
mapTileLayer:hasTileAtX:y:zoomLevel:
returningNO
. - Use the provided disk caching mechanism.