Offline Maps

for Mobile or Tablet

Leaflet + PhoneGap
(or any other Cordova based wrapper)

View the Project on GitHub:
sebastian-meier/OfflineMaps

Why?

If you are developing web-apps for mobile devices wrapped in something like PhoneGap, Icenium, etc. you may come to a point where you want to enable your users to use maps even though they are not online. Especially when you are on vacation in a foreign country or you are just in the underground and have no mobile reception.

There are two ways to achive an offline map. The first is simple, create an mbtile file with TileMill and use the mbutil to extract folders and pngs. This folder can simply be added to your web app and you can define the folder as an url for your leaflet TileLayer. BUT, using this method you end up with a huge amount of files, which need to be copied to your testing device every time you run your application and this takes ages. So for better handling it is adviced to use the mbtiles files created by MapBox for exactly this purpose, better handling. For those who quickly want to know what mbtiles files are: those files are SQLite Databases holding all tile images of your exported map. You can very easily use this Firefox Extension SQLite-Manager to look into an mbtile and see the tiles.

Setup

First of all we need a map. We will use a mbtile file exported from TileMill. The mbtile file in this repository is an export from the Washingtion DC sample included in the TileMill Application. Important: as the javascript sqlite library will automatically add a ".db" extension to your mbtile file name, you need to add this manually to the file itself.

After the map creation we need our native wrapper. In this example we use PhoneGap, but you could essentially use any Cordova (Apache) based wrapper. PhoneGap comes with a command line tool that helps you create your projects:

$ your_phonegap_folder/lib/ios/bin/create new_project_folder com.company.project project_name

After installation we open the project in xCode and add the SQLite Plugin (by pgsqlite) to the plugins folder. Go to config.xml and add these lines (Depending on the PhoneGap version you are using, this might be a plist file):

<feature name="SQLitePlugin">
<param name="ios-package" value="SQLitePlugin" />
</feature>

Now go to your target in xcode and add the "sqlite3.dylib" library to the "Linked Frameworks and Libraries" (see image below). Then add the mbtile file created in the first step to your resources in xcode.

Adding the sqlite library in xCode Step 1 Adding the sqlite library in xCode Step 1
Adding the sqlite library in xCode Step 2 Adding the sqlite library in xCode Step 2

To enable the sqlite library to access our database file, we need to copy it to the right location on startup. Therefore we need to commentout the function "webViewDidStartLoad" and modify it:

- (void) webViewDidStartLoad:(UIWebView*)theWebView
{
    NSString *databaseName = @"map.mbtiles.db";
    
    NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *libraryDir = [libraryPaths objectAtIndex:0];
    
    NSString *databasePath = [libraryDir stringByAppendingPathComponent:@"../Documents/Databases/"];
    NSString *databaseFile = [databasePath stringByAppendingPathComponent:databaseName];
    
    BOOL success;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    success = [fileManager fileExistsAtPath:databasePath];
    if(success) return;
    
    NSString *databasePathFromApp = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:databaseName];
    
    [fileManager createDirectoryAtPath:databasePath withIntermediateDirectories:YES attributes:nil error:NULL];
    [fileManager copyItemAtPath:databasePathFromApp toPath:databaseFile error:nil];
    [fileManager release];
    
    return [super webViewDidStartLoad:theWebView];
}

What we do here is simply checking on every startup of the application if the database file has already been copied to our defined destination or not. If not the file is copied. If your curious about the structur of the apps data directory you can see the structure if you go to:

/Usr/Library/Application Support/iPhone Simulator/iOS_of_you_choice/...

The interesting part is writing the html/javascript code that creates the map and connects it via the sqlite plugin to our mbtiles file. We will use the phonegap sample files. First of all you need to add the leaflet javascript library and stylesheet. You could use the hosted version, but as we want to build an offline app, you should include the latest version from the leaflet website directly into your application. In addition we need the sqlite-plugin-file (included in the pgsqlite's repository) and make sure the cordova library is still in the www folder. The following lines will take care of map & sqlite creation:

onDeviceReady: function() {
  console.log("onDeviceReady");

  //the "db" extension is added by the library
  app.db = window.sqlitePlugin.openDatabase({name: "/Databases/map.mbtiles"});
  
  //lets test if our database works, the following sql query selects our maximum zoom level
  app.db._executeSql(
    //query
    "SELECT DISTINCT zoom_level FROM tiles ORDER BY zoom_level DESC LIMIT 1;", 
    //parameters
    [], 
    //success
    function(res) {
      console.log("success");
      //initialize Leaflet
      app.map = new L.map('map', {center:[38.8977, -77.0365], zoom: 15});
      //create MBTiles Layer
      var tile = new L.TileLayer.MBTiles('', {maxZoom: 19, tms: true, scheme: 'tms', unloadInvisibleTiles:true}, app.db);
      tile.addTo(app.map);

    },
    //error
    function(e) {
      console.log("error");
      console.log(e);
    }
  );

}

Last but not least we need to add the MBTiles Plugin. This plugin was taken from... and slightly modified to work with the sqlite Libraries. Comment: the leaflet documentation says that you can set the layer property tms:true, but i couldn't get it to work. So i just added the lines:

var limit = this._getWrapTileNum();
var y = limit - tilePoint.y - 1;

The numbering of tilemills y-coordinate is in reverse order, this lines take care that the leaflet order is translated into tilemill order.

If everything went smoothly you should now have an offline-map engine that should look like this:

Offline Map App Screenshot Offline Map App Screenshot