Custom Tiled Layer
ArcGIS API for iOS provides a number of predefined layers that work with GIS Servers such as ArcGIS Server, Bing Maps, etc. These layers retrieve map images from the GIS Server and display them on a map. Layers can be broadly categorized into two types - Dynamic layers and Tiled layers. Dynamic layers display maps using images that are generated on the fly. Tiled layers display maps using pre-generated map images, also known as tiles. These tiles are usually hosted on a server and can be accessed via a URL. The format for the URL depends upon the tiling scheme used by the provider to organize the tiles.
The API also provides an extensible framework that allows you to develop your own custom layers. In this post, we will focus on developing a custom tiled layer. Instead of accessing tiles from a server, our layer will use tiles that are bundled with the application. For the sake of brevity, we will only discuss the high-level aspects of creating the layer. The full implementation of the layer is available as a sample.
Map Cache
To use the custom layer, a map cache needs to be embedded in the application bundle.
The map cache contains images. These images are organized within folders representing Levels and Rows. The name of the image files represent Columns. Names for Level folders begin with the letter 'L' followed by a number in decimal format. Names for Row folders and Column images begin with a letter (R for row, and C for column) followed by a number in hexadecimal format. For example, Level 11 is represented by L11 and Row 11 by R0000000b. The cache also contains two XML files - conf.xml and conf.cdi. These files contain metadata about the cache such as the tiling scheme and the full extent.
Note, if your cache was generated with a 9.3.1 ArcGIS Server or older, it may not contain the conf.cdi file. You need to upgrade the service to ArcGIS Server 10. This will automatically create the file for your existing cache.
Also note, ArcGIS Server 10 supports two cache formats - Compact and Exploded. The custom layer discussed in this topic only works with exploded cache format.
When you include a cache in your application, be sure to include the entire folder hierarchy of the cache, starting with the top-level folder. The top-level folder usually has a name beginning with "cache_". The top-level folder contains a folder whose name matches the data frame in the map document that was used to create the cache. The data frame folder usually contains a folder called "_alllayers". In the example shown below, "cache_World" is the top-level folder and "Layers" is the data frame folder.
To add a cache to your application, right-click on the Other Sources group in your XCode project, choose Add > Existing Files. Navigate to the top-level tile cache folder. Enable Copy items into destination group's folder, and select the Create folder references for any added folders option. Click Add to finish.
If you have large number of tiles, it may take a lot of time to copy them into the project directory. To avoid this delay, you can disable the Copy items into destination group's folder option when adding the cache to your application. By doing this, the cache will still get included in your application bundle, but XCode will skip copying it to your project directory.
Map caches can be quite large depending upon factors such as the extent and scale levels of the cache. The size of your application could increase dramatically by including the cache. iOS applications can be as large as 2GB but the application must be under 20MB to allow over-the-air downloads. See the ITunes Connect Developer Guide for more information
Design
All tiled layers in ArcGIS API for iOS extend from the AGSTiledLayer class. This class in-turn extends from AGSLayer. AGSTiledLayer and AGSLayer provide a lot of supporting infrastructure for implementing tiled layers. By inheriting from these classes, we can focus exclusively on the aspects that are unique to our layer. We don't need to worry about, for instance, positioning tiles correctly on the map, or scaling tiles in response to a pinch. All we need to do is provide some metadata about the layer and implement the mechanism for retrieving tiles.
To create our custom layer OfflineTiledLayer, we too will extend from AGSTiledLayer.
@interface OfflineTiledLayer : AGSTiledLayer {
...
}
Any layer that extends AGSTiledLayer must provide information about its tiling scheme and implement the mechanics to retrieve tiles. The tiling scheme is used by the map to calculate which tiles fit within the map's bounds. The map then requests the layer to fetch those specific tiles that need to be displayed.
@implementation OfflineTiledLayer
-(AGSTileInfo*) tileInfo { //todo }
-(NSOperation<AGSTileOperation>*)retrieveImageAsyncForTile:(AGSTile*) tile {
//todo }
...
@end
By virtue of extending AGSTiledLayer, a layer also indirectly extends AGSLayer. The layer needs to provide some basic information such as its fullEnvelope, initialEnvelope, spatialReference, and units. AGSLayer defines read-only properties for these parameters. The custom layer needs to implement getter methods for these properties as shown below.
@implementation OfflineTiledLayer
...
-(AGSUnits) units { //todo }
-(AGSSpatialReference*) spatialReference { //todo }
-(AGSEnvelope*) fullEnvelope { //todo }
-(AGSEnvelope*) initialEnvelope { //todo }
@end
Finally, the layer defines an initialization method that requires a path to the data frame folder in the cache. The method also accepts a reference to an error object so that it can return an error if problems are encountered while reading the cache.
@interface OfflineTiledLayer : AGSTiledLayer {
- (id)initWithDataFramePath:(NSString*)path error:(NSError**)outError;
}
Implementation
The following diagram provides an overview of how the OfflineTiledLayer works .To cleanly separate responsibility, the layer uses a dedicated class called OfflineCacheParserDelegate to parse XML files in the cache. To actually retrieve tiles from the cache, the layer uses objects of an OfflineTileOperation class.
The bulk of the work is done during layer initialization. This is when the layer uses OfflineCacheParserDelegate to parse metadata in the conf.xml and conf.cdi XML files. These files contain information about the layer's tiling scheme and extent respectively.
- (id)initWithDataFramePath: (NSString *)path error:(NSError**)outError {
...
OfflineCacheParserDelegate* parserDelegate = [[OfflineCacheParserDelegate alloc]
init];
//Parse the conf.cdi file
NSString* confCDI = [[NSBundle mainBundle] pathForResource:@"conf"
ofType:@"cdi"
inDirectory:_dataFramePath];
NSXMLParser* xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:[NSURL
fileURLWithPath:confCDI]];
[xmlParser setDelegate:parserDelegate];
[xmlParser parse];
[xmlParser release];
//Parse the conf.xml file
NSString* confXML = [[NSBundle mainBundle] pathForResource:@"conf"
ofType:@"xml"
inDirectory: _dataFramePath];
xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:[NSURL
fileURLWithPath:confXML]];
[xmlParser setDelegate:parserDelegate];
[xmlParser parse];
[xmlParser release];
...
}
OfflineCacheParserDelegate decodes the information and makes it available as AGSTileInfo and AGSEnvelope objects. The layer then uses these objects for implementing the properties defined in AGSLayer and AGSTiledLayer. Finally, the layer indicates it is loaded by invoking the layerDidLoad method.
- (id)initWithDataFramePath: (NSString *)path error:(NSError**)outError {
...
_tileInfo = [[parserDelegate tileInfo] retain];
_fullEnvelope = [[parserDelegate fullEnvelope] retain];
[self layerDidLoad];
...
}
- (AGSTileInfo*) tileInfo { return _tileInfo; }
- (AGSEnvelope*) fullEnvelope { return _fullEnvelope; }
...
When the layer is added to the map, the map requests for tiles that need to be displayed within the map's bounds. For each tile, the map invokes the layer's retrieveImageAsyncForTile: method. To improve performance, tiled layers use an operation queue for retrieving tiles. This operation queue is provided by the parent AGSTiledLayer. The queue allows multiple tiles to be retrieved concurrently in the background. A tiled layer creates an operation object for every tile that needs to be fetched and adds it to the queue. The queue then runs these operations and removes them when they finish executing. Operation objects used by tiled layers must conform to the AGSTileOperation protocol.
OfflineTiledLayer uses a custom class, OfflineTileOperation, to retrieve tiles from the local cache. The operation object is given information about which tile to retrieve, the path to the local cache, and a target-action pair representing the method to invoke when the operation finishes.
- (NSOperation<AGSTileOperation>*) retrieveImageAsyncForTile: (AGSTile*) tile {
//Start an operation to fetch the tile
OfflineTileOperation *operation = [[OfflineTileOperation alloc] initWithTile:tile
dataFramePath:_dataFramePath
target:self
action:@selector(didFinishOperation:)];
[super.operationQueue addOperation:operation];
return [operation autorelease];
}
When the operation finishes, it invokes the didFinishOperation: method on the layer. The layer then checks to see whether the operation found a tile image or not. It then appropriately notifies the tileDelegate. The tileDelegate ensures that the tile is correctly displayed on the map.
- (void) didFinishOperation:(NSOperation<AGSTileOperation>*)op {
//Check if a tile was found...
if (op.tile.image!=nil) {
//... inform the delegate of success
[self.tileDelegate tiledLayer:self operationDidGetTile:op];
} else {
//... inform the delegate of failure
[self.tileDelegate tiledLayer:self operationDidFailToGetTile:op];
}
}
Using the custom layer
To use the layer with your own data, you need to publish the data as a map service on ArcGIS Server and then create a cache for the service. You can then copy the cache over to your development machine and include it in your iOS application.
Once the cache and the custom layer are included in your application, all you need to do is instantiate the layer with a path to your cache's data frame folder and then add the layer to the map -
NSError *err;NSString *dataFramePath = @"cache_World/Layers";
OfflineTiledLayer *tiledLayer = [[OfflineTiledLayer alloc] initWithDataFramePath:
dataFramePath error: &err];
...
UIView *tiledLayerView = [self.mapView addMapLayer:tiledLayer withName:@"Custom Tiled Layer"];