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:
- The amount of text between the lines of actual code is so big, that it makes it hard to follow, but …
- This is necessary because the steps don’t really speak for themselves and …
- 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:
- MBTileProvider.java
- MBTileModuleProvider.java
- 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; } }
You , my friend , are my hero. If there weren’t your MBTiler and BoundingBoxMapView I would probably give up on using OSMDroid
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
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
which version of osmdroid library should be used here.
Current version should work with minor modifications.
Hai friend,
your coding working great .i having small doubt in MBTile loading.how can i load multiple MBTiles in osm.
Haven’t tried this. Also not sure why you’d want that?
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
Sounds like you’re displaying the default tile source that only displays country outlines. Zoom in and use a different source!
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.
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.
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?