This past week I was presented with a typical request, but with an unusual twist. I needed to create a Titanium mobile app which features custom raster and vector layers. The challenge: the map had to be available when the end user was not connected to the Internet. Titanium’s implementation of the Ti.Map object doesn’t support offline maps, so I knew right away that I’d have to find a third party solution. I’m not an Objective C or Java programmer, so writing a custom module was out of the question. That left me one option. I needed to find a JavaScript mapping library which I could present in a webview.
During my research, I came across a blog post that mirrored what I wanted to do. Scott Sheri had created an offline map mobile app using PhoneGap, Leaflet and TileMill which was very similar to what I wanted to accomplish. Seeing how Scott had blazed the trail for me, I decided to follow his path, only using Titanium instead of PhoneGap. I knew there would be a lot of differences in implementation, but it gave me a strong starting point.
Generating the Raster Images with TileMill Years ago, I spent a lot of time working with GIS data, so I have a very strong understanding of how to produce and display spatial data. I chose TileMill to produce my raster layer because it was easy use and readily saved the images in the mbtiles format. The beauty of this is that an .mbtiles file is nothing more than a SQLite database. I could imbed my generated .mbtiles in my application and query it natively using Titanium.
Pay no attention to my horrible choice of colors. I won’t go into details about how I produced this map. TileMill has some pretty good tutorials on their website. Needless to say, what we’re concerned about here is how it’s formatted when it’s exported.
The mbtiles files produced by TileMill are simple SQLite databases. The schema can be found on the MapBox website. To view the data within the file, I’m using SQLite Manager add-on for Firefox.
Now that we have our raster layer, we can work on getting Leaflet to work in a Titanium webview.
Using Leaflet in a Titanium Webview This was relatively simple. I created an HTML page which included all of the JavaScript needed to produce a Leaflet map. I then took the two Leaflet source files (leaflet.js and leaflet.css) and included them in the Titanium /Resources/ directory to allow it to run offline. I then created a Titanium webview to display the page. Quite quickly I had Leaflet up and running in Titanium.
Extending Leaflet to Utilize a SQLite .mbtiles Database Leaflet doesn’t support .mbtiles out of the box. The good news is that because Leaflet is open-source and well documented, it was possible to extend its functionality. I extended L.TileLayer to create a new JavaScript constructor for adding mbtile layers. The new constructor is L.TileLayer.MBTiles.
L.TileLayer was intended to add raster layers where the images are server by a tile server with a standard URL format. (For example, http://{s}.tile.cloudmade.com/[API-key]/997/256/{z}/{x}/{y}.png.) It was going to be necessary to modify this so that the images would instead be drawn from a BLOB field in the .mbtiles SQLite database. This is where my implementation had to differ from Scott Sheri’s. Because Scott was using PhoneGap, he queried the .mbtiles database using a SQLite plugin. With Titanium, I have native database functionality and would be using that instead. The big hurtle was that the main Titanium app and the webview are in different JavaScript contexts and I would have to pass information between the two using the Ti.App.addEventListener() and Ti.App.fireEvent() methods. The fireEvent() method is not asynchronous and does not allow for a callback function.
I overrode three of the L.TileLayer methods (_loadTile(), _createTile(), and _addTilesFromCenterOut()) and added one new method (_onLoad).
_createTile() – Originally, the Leaflet tiles (which are simply img html elements) didn’t have an id attribute that I could reference. This was going to be a necessity because the fireEvent() and addEventListener() methods are not asynchronous. I was going to have to reference the tile by id when the Titanium response for a request for a tile was returned. I simply created a random guid and set it as the id.
_loadTile – I removed the setting of the tile’s src attribute in this method. I placed the relevant information needed for requesting tiles (i.e. x, y, z, and id) in an array named aUpdate. When loaded, the map will consist of blank tiles and they will be populated by Titanium through a later request.
_addTilesFromCenterOut() – Leaflet uses a document fragment to load the img tiles to the DOM. I had to wait until this document fragment was added before I could send the request to Titanium to query for the actual image data. At the end of this method, I just added a call to _onLoad().
_onLoad() – This new method will fire once the blank map tiles are added to the DOM. It calls a Titanium application level event named getMbTiles. It passes a JSON form of the aUpdates array.
The getMbTiles event triggers the query of the database for the needed tiles. SELECT tile_data FROM images INNER JOIN map ON images.tile_id = map.tile_id WHERE zoom_level = ? AND tile_column = ? AND tile_row = ? These tiles are then base64 encoded and sent back to the webview using a custom event named receiveTileUrl. receiveTileUrl sets the src of the various tiles to the appropriate image data.
Source Files Leaflet Demo (does not include mbtiles file) .zip MasterView.js map.html
The Offline Map! Ta-da! The images are now shown on the Leaflet map imbedded in a Titanium webview. By turning on airplane mode will show that all the data and code is hosted locally and that no Internet connection is required to use this map.
Next Steps Now, this is just a proof-of-concept and is not production ready. The current obstacle is the size of the .mbtiles file. My simple demonstration map was about 4mb and contained very little visual data. A more robust raster layer can easily exceed the 50mb limit imposed by the Apple App Store. A strategy needs to be developed where the end user can pick and choose what data they will need in the field and download it accordingly through the app. But that’s another blog entry…