Local MBTiles

This sample extends the abstract base class TiledServiceLayer and implements the getTile() method to fetch MBTiles from a SQLite Database.

Sample Design

MBTiles format

Map Box is a popular open source tool for publishing tile based web maps. It uses a tiling scheme similar to Open Street Maps known as the Tile Map Service (TMS) specification. Instead of storing its tiles in a compact cache format like Esri, their tools store the tiles in a SQLite database. The full specification is available here.

The TMS Specification is similar to other web map tiling schemes used by ArcGIS Online®, Google Maps®, or Open Street Maps. For convenience, the pre-rendered image tiles are stored in a SQL database in a table named tiles. The table has three integer fields named zoom_level, tile_column, and tile_row. The last field is a blob named tile_data which holds the PNG or JPEG tile image.

MBTilesLayer

The ArcGIS Runtime SDK for Android introduced TiledServiceLayer abstract base class in version 10.1.1. It is used as the base class for ArcGISTiledMapService, BingMapsLayer, and OpenStreetMapLayer. The class exposes a protected abstract method, getTile(), to fetch tiles. This sample extends this class and implements the getTile() method to create a layer by fetching MBTiles from a SQLite database.

LocalMBTiles

To use the MBTilesLayer class just construct the layer, passing the location of the MBTiles SQLite database and add the layer to the MapView. The LocalMBTiles class is an sample usage of the MBTilesLayer based local data stored on device.

Provision your device

  1. Download the data from ArcGIS Online.
  2. Extract the contents of the downloaded zip file to disk.
  3. Create an ArcGIS/samples/mbtiles folder on your device. This requires you to use the Android Debug Bridge (adb) tool found in <sdk-dir>/platform-tools.
  4. Open up a command prompt and execute the adb shell command to start a remote shell on your target device.
  5. Navigate to your sdcard directory, e.g. cd /storage/sdcard0/.
  6. Create the ArcGIS/samples/mbtiles directory, mkdir ArcGIS/samples/mbtiles.
  7. You should now have the following directory on your target device, /storage/sdcard0/ArcGIS/samples/mbtiles. We will copy the contents of the downloaded data into this directory. Note: Directory may be slightly different on your device.
  8. Exit the shell with the, exit command.
  9. While still in your command prompt, navigate to the folder where you extracted the contents of the data from step 1 and execute the following command:
    • adb push world_countries.mbtiles /storage/sdcard0/ArcGIS/samples/mbtiles

Using the Sample

Once you have the sample deployed to your device you can interact with the sample with the following:

  1. The map will display the World Streets Basemap with the MBTiles layer overlayed.
  2. Double tap on the map to zoom in and see the zoom level dependency on the overlayed MBTiles.

Sample Code

/* Copyright 2014 ESRI
 *
 * All rights reserved under the copyright laws of the United States
 * and applicable international laws, treaties, and conventions.
 *
 * You may freely redistribute and use this sample code, with or
 * without modification, provided you include the original copyright
 * notice and use restrictions.
 *
 * See the Sample code usage restrictions document for further information.
 *
 */

package com.esri.android.samples.mbtiles;

import android.app.Activity;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Environment;
import android.widget.Toast;

import com.esri.android.map.MapView;
import com.esri.android.map.ags.ArcGISTiledMapServiceLayer;

public class LocalMBTiles extends Activity {
  MapView mMapView = null;
  ArcGISTiledMapServiceLayer tileLayer;
  boolean activeNetwork;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // Retrieve the map and initial extent from XML layout
    mMapView = (MapView) findViewById(R.id.map);

    // create an ArcGISTiledMapServiceLayer as a background if network available
    activeNetwork = isNetworkAvailable();
    if (activeNetwork) {
      tileLayer = new ArcGISTiledMapServiceLayer(
          "http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer");
      // Add tiled layer to MapView
      mMapView.addLayer(tileLayer);
    }else{
      Toast toast = Toast.makeText(this, R.string.offline_message, Toast.LENGTH_SHORT);
      toast.show();
    }

    // Add a MBTilesLayer on top with 50% opacity
    MBTilesLayer mbLayer = new MBTilesLayer(Environment.getExternalStorageDirectory().getPath()
        + "/ArcGIS/samples/mbtiles/world_countries.mbtiles");
    mbLayer.setOpacity(0.5f);
    mMapView.addLayer(mbLayer);
    // enable map to wrap around
    mMapView.enableWrapAround(true);

  }

  private boolean isNetworkAvailable() {
    ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();

    if (netInfo != null && netInfo.isConnected()) {
      return true;
    }

    return false;
  }

  @Override
  protected void onPause() {
    super.onPause();
    mMapView.pause();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mMapView.unpause();
  }

}
/* Copyright 2012 ESRI
 *
 * All rights reserved under the copyright laws of the United States
 * and applicable international laws, treaties, and conventions.
 *
 * You may freely redistribute and use this sample code, with or
 * without modification, provided you include the original copyright
 * notice and use restrictions.
 *
 * See the Sample code usage restrictions document for further information.
 *
 */

package com.esri.android.samples.mbtiles;

import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import com.esri.android.map.TiledServiceLayer;
import com.esri.core.geometry.Envelope;
import com.esri.core.geometry.GeometryEngine;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.SpatialReference;

/**
 * The MBTilesLayer class allows you to work with a MBTiles stored in a SQLite
 * database.
 * 
 */
public class MBTilesLayer extends TiledServiceLayer {

  private SQLiteDatabase mapDb;
  private int mLevels = 0;

  /**
   * The constructor to instantiate MBTiles from a path on device
   * 
   * @param path
   *          path is expected to be of the form /sdcard/path/package.mbtiles
   */
  public MBTilesLayer(String path) {
    super(path);
    try {
      mapDb = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
    } catch (SQLException ex) {
      Log.e(this.getName(), ex.getMessage());
      throw (ex);
    }

    // Default TMS bounds = bounds of Web Mercator projection
    Envelope envWGS = new Envelope(-180.0, -85.0511, 180.0, 85.0511);

    // See if the MBTiles DB defines their own Bounds in the metadata table
    Cursor bounds = mapDb.rawQuery("SELECT value FROM metadata WHERE name = 'bounds'", null);
    if (bounds.moveToFirst()) {
      String bs = bounds.getString(0);
      String[] ba = bs.split(",", 4);
      if (ba.length == 4) {
        double leftLon = Double.parseDouble(ba[0]);
        double topLat = Double.parseDouble(ba[3]);
        double rightLon = Double.parseDouble(ba[2]);
        double bottomLat = Double.parseDouble(ba[1]);

        envWGS = new Envelope(leftLon, bottomLat, rightLon, topLat);
      }
    }

    Envelope envWeb = (Envelope) GeometryEngine.project(envWGS, SpatialReference.create(4326),
        SpatialReference.create(3857));

    Point origin = envWeb.getUpperLeft();

    Cursor maxLevelCur = mapDb.rawQuery("SELECT MAX(zoom_level) AS max_zoom FROM tiles", null);
    if (maxLevelCur.moveToFirst()) {
      mLevels = maxLevelCur.getInt(0);
    }

    Log.i("TAG", "Max levels = " + Integer.toString(mLevels));

    double[] resolution = new double[mLevels];
    double[] scale = new double[mLevels];
    for (int i = 0; i < mLevels; i++) {
      // see the TMS spec for derivation of the level 0 scale and resolution
      // For each level the resolution (in meters per pixel) doubles
      resolution[i] = 156543.032 / Math.pow(2, i);
      // Level 0 scale is 1:554,678,932. Each level doubles this.
      scale[i] = 554678932 / Math.pow(2, i);
    }

    /*
     * Note, the constructor must set the following values or we won't send the
     * status change events to listeners and the tiles will not be fetched
     * 
     * Origin is Top Left (web Mercator) , the rest are defined by the TMS
     * Global-mercator spec (scales, resolution, 96dpi 256x256 pixel tiles) See:
     * http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-mercator
     */
    TileInfo ti = new TileInfo(origin, scale, resolution, mLevels, 96, 256, 256);

    this.setTileInfo(ti);
    this.setFullExtent(envWeb);
    this.setDefaultSpatialReference(SpatialReference.create(3857));
    this.setInitialExtent(envWeb);

    this.initLayer();

  }

  @Override
  protected byte[] getTile(int level, int col, int row) throws Exception {

    // need to flip origin
    int nRows = (1 << level); // Num rows = 2^level
    int tmsRow = nRows - 1 - row;

    Cursor imageCur = mapDb.rawQuery("SELECT tile_data FROM tiles WHERE zoom_level = " + Integer.toString(level)
        + " AND tile_column = " + Integer.toString(col) + " AND tile_row = " + Integer.toString(tmsRow), null);
    if (imageCur.moveToFirst()) {
      return imageCur.getBlob(0);
    }
    return null; // Alternatively we might return a "no data" tile
  }

}
Feedback on this topic?