package com.mapbox.mapboxsdk.tileprovider.tilesource; import android.content.Context; import android.content.res.AssetManager; import android.database.sqlite.SQLiteDatabase; import android.os.Environment; import android.util.Log; import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.tileprovider.MapTile; import com.mapbox.mapboxsdk.tileprovider.modules.MBTilesFileArchive; import com.mapbox.mapboxsdk.tileprovider.modules.MapTileDownloader; import com.mapbox.mapboxsdk.views.util.constants.MapViewConstants; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import uk.co.senab.bitmapcache.CacheableBitmapDrawable; /** * A layer that pulls resources from an MBTiles file. Used for offline map tiles, * like those generated by TileMill. */ public class MBTilesLayer extends TileLayer implements MapViewConstants, MapboxConstants { private static final String TAG = "MBTilesLayer"; MBTilesFileArchive mbTilesFileArchive; /** * Initialize a new tile layer, represented by a MBTiles file. * * @param url path to a MBTiles file * @param context the graphics drawing context */ public MBTilesLayer(final Context context, final String url) { super(url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')), url); initialize(url, context); } /** * Initialize a new tile layer, represented by a MBTiles file. * This constructor does need a context but as a consequence won't look * for an asset mbtiles * * @param url path to a MBTiles file */ public MBTilesLayer(final String url) { this(null, url); } /** * Initialize a new tile layer, represented by a MBTiles file. * * @param file a MBTiles file */ public MBTilesLayer(final File file) { super(file.getName(), file.getAbsolutePath()); initialize(file); } /** * Initialize a new tile layer, represented by a Database file. * * @param db a database used as the MBTiles source */ public MBTilesLayer(final SQLiteDatabase db) { super(getFileName(db.getPath()), db.getPath()); initialize(db); } /** * Get the filename of this layer based on the full path * * @param path * @return the filename of the backing mbtiles file */ private static final String getFileName(final String path) { return path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.')); } /** * Creates a file from an input stream by reading it byte by byte. * todo: same as MapViewFactory's createFileFromInputStream */ private static File createFileFromInputStream(InputStream inputStream, String URL) { try { File f = new File(URL); OutputStream outputStream = new FileOutputStream(f); byte[] buffer = new byte[1024]; int length = 0; while ((length = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, length); } outputStream.close(); inputStream.close(); return f; } catch (IOException e) { Log.e(TAG, "Failed to create file from input stream.", e); } return null; } /** * Reads and opens a MBTiles file and loads its tiles into this layer. * * @param file */ private void initialize(File file) { if (file != null) { mbTilesFileArchive = MBTilesFileArchive.getDatabaseFileArchive(file); } if (mbTilesFileArchive != null) { mMaximumZoomLevel = mbTilesFileArchive.getMaxZoomLevel(); mMinimumZoomLevel = mbTilesFileArchive.getMinZoomLevel(); mName = mbTilesFileArchive.getName(); mDescription = mbTilesFileArchive.getDescription(); mAttribution = mbTilesFileArchive.getAttribution(); mBoundingBox = mbTilesFileArchive.getBounds(); mCenter = mbTilesFileArchive.getCenter(); } } /** * Reads and opens a MBTiles file given by url and loads its tiles into this layer. */ private void initialize(final SQLiteDatabase db) { if (db != null) { mbTilesFileArchive = new MBTilesFileArchive(db); } if (mbTilesFileArchive != null) { mMaximumZoomLevel = mbTilesFileArchive.getMaxZoomLevel(); mMinimumZoomLevel = mbTilesFileArchive.getMinZoomLevel(); mName = mbTilesFileArchive.getName(); mDescription = mbTilesFileArchive.getDescription(); mAttribution = mbTilesFileArchive.getAttribution(); mBoundingBox = mbTilesFileArchive.getBounds(); mCenter = mbTilesFileArchive.getCenter(); } } /** * Reads and opens a MBTiles file given by url and loads its tiles into this layer. */ private void initialize(String url, final Context context) { initialize(getFile(url, context)); } private File getFile(String url, final Context context) { if (context != null) { //we assume asset here AssetManager am = context.getAssets(); InputStream inputStream; try { inputStream = am.open(url); final File mbTilesDir; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || (!Environment.isExternalStorageRemovable())) { mbTilesDir = new File(context.getExternalFilesDir(null), url); } else { mbTilesDir = new File(context.getFilesDir(), url); } return createFileFromInputStream(inputStream, mbTilesDir.getPath()); } catch (IOException e) { Log.e(TAG, "MBTiles file not found in assets: " + e.toString()); return null; } } try { return new File(url); } catch (Exception e) { Log.e(TAG, "can't load MBTiles: " + e.toString()); return null; } } @Override public void detach() { if (mbTilesFileArchive != null) { mbTilesFileArchive.close(); mbTilesFileArchive = null; } } @Override public CacheableBitmapDrawable getDrawableFromTile(final MapTileDownloader downloader, final MapTile aTile, boolean hdpi) { if (mbTilesFileArchive != null) { InputStream stream = mbTilesFileArchive.getInputStream(this, aTile); if (stream != null) { CacheableBitmapDrawable result = downloader.getCache().putTileStream(aTile, stream, null); if (result == null) { Log.d(TAG, "error reading stream from mbtiles"); } return result; } } return null; } }