Using MBTiles in osmdroid

A couple of days ago I posted an example of getting osmdroid to work with an offline MBTiles database. The example is quite elaborate, mostly because 80% of it is comment, not code. I found three issues with this:

  1. The amount of text between the lines of actual code is so big, that it makes it hard to follow, but …
  2. This is necessary because the steps don’t really speak for themselves and …
  3. Even if you remove all the comments, you still end up with more lines of code than you should

So I thought I’d remedy this by extending the framework. As a result I updated RouteMapActivity.java and created three new classes:

  1. MBTileProvider.java
  2. MBTileModuleProvider.java
  3. MBTileSource.java

Below you’ll find the code to these classes. Please note that you have to make sure that they are put in the correct package for things to work. You can also download the sources.

RouteMapActivity.java

/**
 * Created on August 12, 2012
 * 
 * @author Melle Sieswerda
 */
package com.example.yourproject;

import java.io.File;

import org.osmdroid.DefaultResourceProxyImpl;
import org.osmdroid.tileprovider.IRegisterReceiver;
import org.osmdroid.views.MapController;
import org.osmdroid.views.MapView;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.view.Menu;

import com.example.yourproject.osmdroid.tileprovider.MBTileProvider;

public class RouteMapActivity extends Activity implements IRegisterReceiver {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create the mapView with an MBTileProvider
        DefaultResourceProxyImpl resProxy;
        resProxy = new DefaultResourceProxyImpl(this.getApplicationContext());

        String packageDir = "/com.example.yourproject";
        String path = Environment.getExternalStorageDirectory() + packageDir;
        File file = new File(path, "HollandRoute.mbtiles");

        MBTileProvider provider = new MBTileProvider(this, file);
        MapView mapView = new MapView(this,
                                      provider.getTileSource()
                                              .getTileSizePixels(),
                                      resProxy,
                                      provider);

        mapView.setBuiltInZoomControls(true);

        // Zoom in and go to Amsterdam
        MapController controller = mapView.getController();
        controller.setZoom(12);
        controller.animateTo(new LatLonPoint(52.373444, 4.892229));

        // Set the MapView as the root View for this Activity; done!
        setContentView(mapView);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_route_map, menu);
        return true;
    }

}

MBTileProvider.java

/**
 * Created on August 12, 2012
 * 
 * @author Melle Sieswerda
 * 
 */
package com.example.yourproject.osmdroid.tileprovider;

import java.io.File;
import java.util.Collections;

import org.osmdroid.tileprovider.IRegisterReceiver;
import org.osmdroid.tileprovider.MapTileProviderArray;
import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase;

import com.example.yourproject.osmdroid.tileprovider.modules.MBTileModuleProvider;
import com.example.yourproject.osmdroid.tileprovider.tilesource.MBTileSource;

/**
 * This class is a simplification of the the MapTileProviderArray: it only
 * allows a single provider.
 */
public class MBTileProvider extends MapTileProviderArray {

    public MBTileProvider(IRegisterReceiver receiverRegistrar, File file) {

        /**
         * Call the super-constructor.
         * 
         * MapTileProviderBase requires a TileSource. As far as I can tell it is
         * only used in its method rescaleCache(...) to get the pixel size of a
         * tile. It seems to me that this is inappropriate, as a MapTileProvider
         * can have multiple sources (like the module array defined below) and
         * therefore multiple tileSources which might return different values!!
         * 
         * If the requirement is that the tile size is equal across tile
         * sources, then the parameter should be obtained from a different
         * location, From TileSystem for example.
         */
        super(MBTileSource.createFromFile(file), receiverRegistrar);

        // Create the module provider; this class provides a TileLoader that
        // actually loads the tile from the DB.
        MBTileModuleProvider moduleProvider;
        moduleProvider = new MBTileModuleProvider(receiverRegistrar,
                                                  file,
                                                  (MBTileSource) getTileSource());

        MapTileModuleProviderBase[] pTileProviderArray;
        pTileProviderArray = new MapTileModuleProviderBase[] { moduleProvider };

        // Add the module provider to the array of providers; mTileProviderList
        // is defined by the superclass.
        Collections.addAll(mTileProviderList, pTileProviderArray);
    }
    
    // TODO: implement public Drawable getMapTile(final MapTile pTile) {}
    //       The current implementation is needlessly complex because it uses
    //       MapTileProviderArray as a basis.

}

MBTileModuleProvider.java

/**
 * Created on August 12, 2012
 * 
 * @author Melle Sieswerda
 */
package com.example.yourproject.osmdroid.tileprovider.modules;

import java.io.File;
import java.io.InputStream;

import org.osmdroid.tileprovider.IRegisterReceiver;
import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.MapTileRequestState;
import org.osmdroid.tileprovider.modules.MapTileFileStorageProviderBase;
import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.util.StreamUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import android.graphics.drawable.Drawable;

import com.example.yourproject.osmdroid.tileprovider.tilesource.MBTileSource;

public class MBTileModuleProvider extends MapTileFileStorageProviderBase {

    private static final Logger logger = LoggerFactory.getLogger(MBTileModuleProvider.class);

    protected MBTileSource tileSource;

    /**
     * Constructor
     * 
     * @param pRegisterReceiver
     * @param file
     * @param tileSource
     */
    public MBTileModuleProvider(IRegisterReceiver receiverRegistrar,
                                File file,
                                MBTileSource tileSource) {

        // Call the super constructor
        super(receiverRegistrar,
              NUMBER_OF_TILE_FILESYSTEM_THREADS,
              TILE_FILESYSTEM_MAXIMUM_QUEUE_SIZE);

        // Initialize fields
        this.tileSource = tileSource;

    }

    @Override
    protected String getName() {
        return "MBTiles File Archive Provider";
    }

    @Override
    protected String getThreadGroupName() {
        return "mbtilesarchive";
    }

    @Override
    protected Runnable getTileLoader() {
        return new TileLoader();
    }

    @Override
    public boolean getUsesDataConnection() {
        return false;
    }

    @Override
    public int getMinimumZoomLevel() {
        return tileSource.getMinimumZoomLevel();
    }

    @Override
    public int getMaximumZoomLevel() {
        return tileSource.getMaximumZoomLevel();
    }

    @Override
    public void setTileSource(ITileSource tileSource) {
        logger.warn("*** Warning: someone's trying to reassign MBTileModuleProvider's tileSource!");
        if (tileSource instanceof MBTileSource) {
            this.tileSource = (MBTileSource) tileSource;
        } else {
            logger.warn("*** Warning: and it wasn't even an MBTileSource! That's just rude!");
            
        }
    }

    private class TileLoader extends MapTileModuleProviderBase.TileLoader {

        @Override
        public Drawable loadTile(final MapTileRequestState pState) {

            // if there's no sdcard then don't do anything
            if (!getSdCardAvailable()) {
                return null;
            }

            MapTile pTile = pState.getMapTile();
            InputStream inputStream = null;

            try {
                inputStream = tileSource.getInputStream(pTile);
                
                if (inputStream != null) {
                    Drawable drawable = tileSource.getDrawable(inputStream);

                    // Note that the finally clause will be called before
                    // the value is returned!
                    return drawable;
                }

            } catch (Throwable e) {
                logger.error("Error loading tile", e);

            } finally {
                if (inputStream != null) {
                    StreamUtils.closeStream(inputStream);
                }
            }

            return null;
        }
    }

}

MBTileSource.java

/**
 * Created on August 12, 2012
 * 
 * @author Melle Sieswerda
 */
package com.example.yourproject.osmdroid.tileprovider.tilesource;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;

import org.osmdroid.ResourceProxy;
import org.osmdroid.ResourceProxy.string;
import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.tilesource.BitmapTileSourceBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

public class MBTileSource extends BitmapTileSourceBase {

    // Log log log log ...
    private static final Logger logger = LoggerFactory.getLogger(MBTileSource.class);

    // Database related fields
    public final static String TABLE_TILES = "tiles";
    public final static String COL_TILES_ZOOM_LEVEL = "zoom_level";
    public final static String COL_TILES_TILE_COLUMN = "tile_column";
    public final static String COL_TILES_TILE_ROW = "tile_row";
    public final static String COL_TILES_TILE_DATA = "tile_data";

    protected SQLiteDatabase database;
    protected File archive;

    // Reasonable defaults ..
    public static final int minZoom = 8;
    public static final int maxZoom = 15;
    public static final int tileSizePixels = 256;

    // Required for the superclass
    public static final string resourceId = ResourceProxy.string.offline_mode;

    /**
     * The reason this constructor is protected is because all parameters,
     * except file should be determined from the archive file. Therefore a
     * factory method is necessary.
     * 
     * @param minZoom
     * @param maxZoom
     * @param tileSizePixels
     * @param file
     */
    protected MBTileSource(int minZoom,
                           int maxZoom,
                           int tileSizePixels,
                           File file,
                           SQLiteDatabase db) {
        super("MBTiles", resourceId, minZoom, maxZoom, tileSizePixels, ".png");

        archive = file;
        database = db;
    }

    /**
     * Creates a new MBTileSource from file.
     * 
     * Parameters minZoom, maxZoom en tileSizePixels are obtained from the
     * database. If they cannot be obtained from the DB, the default values as
     * defined by this class are used.
     * 
     * @param file
     * @return
     */
    public static MBTileSource createFromFile(File file) {
        SQLiteDatabase db;
        int flags = SQLiteDatabase.NO_LOCALIZED_COLLATORS | SQLiteDatabase.OPEN_READONLY;

        int value;
        int minZoomLevel;
        int maxZoomLevel;
        int tileSize = tileSizePixels;
        InputStream is = null;

        // Open the database
        db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, flags);

        // Get the minimum zoomlevel from the MBTiles file
        value = getInt(db, "SELECT MIN(zoom_level) FROM tiles;");
        minZoomLevel = value > -1 ? value : minZoom;

        // Get the maximum zoomlevel from the MBTiles file
        value = getInt(db, "SELECT MAX(zoom_level) FROM tiles;");
        maxZoomLevel = value > -1 ? value : maxZoom;

        // Get the tile size
        Cursor cursor = db.rawQuery("SELECT tile_data FROM images LIMIT 0,1",
                                    new String[] {});

        if (cursor.getCount() != 0) {
            cursor.moveToFirst();
            is = new ByteArrayInputStream(cursor.getBlob(0));

            Bitmap bitmap = BitmapFactory.decodeStream(is);
            tileSize = bitmap.getHeight();
            logger.debug(String.format("Found a tile size of %d", tileSize));
        }

        cursor.close();
        // db.close();

        return new MBTileSource(minZoomLevel, maxZoomLevel, tileSize, file, db);
    }

    protected static int getInt(SQLiteDatabase db, String sql) {
        Cursor cursor = db.rawQuery(sql, new String[] {});
        int value = -1;

        if (cursor.getCount() != 0) {
            cursor.moveToFirst();
            value = cursor.getInt(0);
            logger.debug(String.format("Found a minimum zoomlevel of %d", value));
        }

        cursor.close();
        return value;
    }

    public InputStream getInputStream(MapTile pTile) {

        try {
            InputStream ret = null;
            final String[] tile = { COL_TILES_TILE_DATA };
            final String[] xyz = { Integer.toString(pTile.getX()),
                                   Double.toString(Math.pow(2, pTile.getZoomLevel()) - pTile.getY() - 1),
                                   Integer.toString(pTile.getZoomLevel()) };

            final Cursor cur = database.query(TABLE_TILES,
                                              tile,
                                              "tile_column=? and tile_row=? and zoom_level=?",
                                              xyz,
                                              null,
                                              null,
                                              null);

            if (cur.getCount() != 0) {
                cur.moveToFirst();
                ret = new ByteArrayInputStream(cur.getBlob(0));
            }
            
            cur.close();
            
            if (ret != null) {
                return ret;
            }
            
        } catch (final Throwable e) {
            logger.warn("Error getting db stream: " + pTile, e);
        }

        return null;

    }

}

Tagged , . Bookmark the permalink.

12 Responses to Using MBTiles in osmdroid

  1. dimy93 says:

    You , my friend , are my hero. If there weren’t your MBTiler and BoundingBoxMapView I would probably give up on using OSMDroid

  2. Ignacio says:

    Works great!, but I had to modify the line of MBTileSource.java:

    Cursor cursor = db.rawQuery(“SELECT tile_data FROM images LIMIT 0,1″, new String[] {});

    for:

    Cursor cursor = db.rawQuery(“SELECT tile_data FROM tiles LIMIT 0,1″, new String[] {});

    thanks

    • Patrick says:

      Hello Ignacio. I change this and now i not recieve error. But now I have 2 errors:

      controller.animateTo(new LatLonPoint(52.373444, 4.892229));

      and

      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
      getMenuInflater().inflate(R.menu.activity_route_map, menu);
      return true;
      }

      At moment, I only see empty tiles and I dont see my mbtiles. Some ideas? Thanks iu very much

  3. salman says:

    which version of osmdroid library should be used here.

  4. sathish says:

    Hai friend,

    your coding working great .i having small doubt in MBTile loading.how can i load multiple MBTiles in osm.

  5. Alice Wang says:

    Hello

    I have tried your example, it is working fine except the map is displayed repeatedly. I want to display a world map, but I end up with having like over 100 maps displayed on my fragment. And I also tried setScrollableAreaLimit() and BoundMapView, still does not work.
    Do you have any idea about this? Thanks a lot

    Best Regards
    Alice

  6. Pan says:

    Hello,
    I am using Android studio. First of all i get my libs no to source.
    I download my .mbtiles from MOBAC and i save them to my device(sdcard/osmdroid/tiles/mytiles.mbtiles).
    The package is correct.
    I follow the tutorial step by step but when i run my app it says “Unfornutately MyApp has stopped.
    Has anyone have an idea why this happens?
    No errors in the code.

    • melle says:

      I assume “No errors in the code” means your program compiles and is syntactically correct.
      What you’re describing is a runtime error: so check the stack trace in Logcat/Android Studio.

      • Pan says:

        Firstly thanks for the perfect tutorial and also for your interest.

        I change this part

        String packageDir = “/com.example.yourproject”;
        String path = Environment.getExternalStorageDirectory() + packageDir;
        File file = new File(path, “HollandRoute.mbtiles”);
        String path=”osmdroid”;

        to
        String path=”osmdroid”;
        String sdcard = Environment.getExternalStorageDirectory()+ path;
        String db_name = “mb.mbtiles”;
        File file = new File(sdcard, db_name); // sqlite file to load

        and now my device shows a white screen and i have this error:
        ” /storage/emulated/0osmdroid/mb.mbtiles PATH:/storage/emulated/0osmdroid/mb.mbtiles”

        i save my mb.mbtiles to mydevice (sdcard/osmdroid)

        i try different changes with the path like “/osmdroid”, “osmdroid/tiles”,etc) but still a white screen.
        Please can you help me?

Leave a Reply to Ignacio Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>