I write solutions to the problems I can't find much about elsewhere on the Web, also some code/script snippets that are absolutely awesome and make my life easier. Will be glad if someone finds these posts interesting and helpful!

Tuesday, June 7, 2011

Offline tiled layer with ArcGIS for Android

On the official ArcGIS mobile blog they have described how to create an offline tiled layer for iOS, but not a word about Android (probably because it's still in beta, thing might change and it doesn't gain any publicity anyway). The idea was taken from this blog: if not for that, I would've not even attempted to look into this direction. So thank God for a custom hack!
My first attempt for offline tiles was to create a local tile server and in the ArcGISTiledMapServiceLayer as a URL supply something with localhost in it. It totally worked, but there were two issues that forced me to look for another solution. Number one was technical: as soon as the connection was down (e.g. Airplane mode turned on) the tiles would just stop to download (even though their physical location was on the same SD card the application was installed on!); the second issue was mental: just for knowing that the files stored locally had to take a trip to the moon before being rendered (let alone maintaining a whole process of a local map server), caused an allergy and made me invest some time into research.
So here we go with offline tiled layer. Easier than easy, with a very little code, but totally impossible to find out how to make it! At least at the current version of ArcGIS for Android all the interesting stuff regarding custom tiled layers is undocumented.
Here's the implementation of the custom tiled layer:
package com.aploon.map.esri;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonParser;
import android.content.Context;
import android.util.AttributeSet;
import com.esri.android.map.TiledLayer;
import com.esri.core.map.TiledLayerModel;
public class OfflineTiledLayer extends TiledLayer {
public OfflineTiledLayer(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
}
public OfflineTiledLayer(Context paramContext,
AttributeSet paramAttributeSet, int paramInt) {
super(paramContext, paramAttributeSet, paramInt);
}
File workingDirectory;
String relativePath;
String tileServerParamJsonPath;
String tilesPath;
public OfflineTiledLayer(Context paramContext, File workingDirectory, String relativePath, String tileServerParamJsonPath, String tilesPath) {
super(paramContext, null);
this.workingDirectory = workingDirectory;
this.tileServerParamJsonPath = tileServerParamJsonPath;
this.relativePath = relativePath;
this.tilesPath = tilesPath;
}
protected TiledLayerModel initModel() throws Exception {
//read into JsonParser the specification of this map server (spatial reference, full extent, image parameters etc)
JsonParser paramJsonParser = new JsonFactory().createJsonParser(new File(workingDirectory.getAbsolutePath() + relativePath + tileServerParamJsonPath));
paramJsonParser.nextToken();//advance the json parser to the beginning, because the next call will check whether cursor is in the start_object or not, and if not it will return null
//this call actually parses the json specification into a MapServer instance (it's just a bean holding values)
com.esri.core.internal.d.g mapServer = com.esri.core.internal.d.g.a(paramJsonParser, null);
//create the model; spatial reference and envelope were somewhat easy to find out, but TileInfo is an internal class
// spatial ref envelope tileinfo
model = new TiledLayerModel(mapServer.g(), mapServer.k(), mapServer.i()) {
private static final long serialVersionUID = 1L;
/**
* This is the main method. It returns the tile as an array of bytes for requested level, row, column.
*/
public byte[] getTile(int lev, int row, int col) throws Exception {
File requestedFile = new File(workingDirectory.getAbsolutePath() + relativePath + tilesPath + lev + "/" + row + "/" + col);
FileInputStream requestedFileStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {//attempt to read the file requested and write it to the output
requestedFileStream = new FileInputStream(requestedFile);
int count = 8192;
int offset = 0;
byte[] buffer = new byte[count];
int length = -1;
while((length = requestedFileStream.read(buffer, offset, count)) > -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
}
catch(Exception e) {
return new byte[0];
}
finally {//close streams
if(outputStream != null) {
outputStream.close();
}
if(requestedFileStream != null) {
requestedFileStream.close();
}
}
}
};
//set initial extent to the model
// extent spatial ref
((TiledLayerModel) model).setExtent(mapServer.j(), 400, 400, mapServer.l());
return (TiledLayerModel) model;
}
}
Here's a usage example:
map = (MapView) findViewById(R.id.map); //get the map instance
TiledLayer DAY_LAYER = new OfflineTiledLayer(this, new File(Environment.getExternalStorageDirectory(), "services"), "/RoadMapsWebMercator101010/MapServer/", "index.html", "/Day/tile/"); //create a layer
map.addLayer(DAY_LAYER); //add this layer to the map
view raw gistfile1.java hosted with ❤ by GitHub

Now about the filesystem. In the usage example I have set some values to the OfflineTiledLayer constructor and these represent the directory structure. First off, I didn't use the cache created by ArcGIS. I mentioned in the beginning of the post that earlier I had implemented a local map server, that's why the file paths reflect the ones used by the online map servers. For instance, the absolute path of the tile at zoom level 3/row 44/column 65 looks like /mnt/sdcard/services/RoadMapsWebMercator101010/MapServer/tile/3/44/65. I guess it's not hard at all to modify my class to use the HEX paths of the cache created by ArcGIS.
Finally, a couple words about the map server specification. That paramter index.html of the OfflineTiledLayer constructor represents this. Again, I named it index.html to support the local map server implemented earlier, but in fact it contains what an online map server outputs by clicking on the link "REST" on the bottom of the MapServer description page of the ArcGIS server (the address looks something like /MapServer?f=json&pretty=true). 

And that's totally it. Hope it helps!