package org.osmdroid.tileprovider.modules; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.util.Log; import org.osmdroid.api.IMapView; import org.osmdroid.config.Configuration; import org.osmdroid.tileprovider.ExpirableBitmapDrawable; import org.osmdroid.tileprovider.IRegisterReceiver; import org.osmdroid.tileprovider.MapTile; import org.osmdroid.tileprovider.MapTileProviderBase; import org.osmdroid.tileprovider.MapTileRequestState; import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; import org.osmdroid.tileprovider.tilesource.ITileSource; import org.osmdroid.tileprovider.util.Counters; import org.osmdroid.tileprovider.util.StreamUtils; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.concurrent.atomic.AtomicReference; /** * Sqlite based tile cache mechansism * * @since 5.1 * @see SqlTileWriter * Created by alex on 1/16/16. */ public class MapTileSqlCacheProvider extends MapTileFileStorageProviderBase{ // =========================================================== // Constants // =========================================================== // =========================================================== // Fields // =========================================================== private final AtomicReference<ITileSource> mTileSource = new AtomicReference<ITileSource>(); private SqlTileWriter mWriter; private final long mMaximumCachedFileAge; //FIXME constants with #348 private static final String[] tile = {"tile"}; private static final String[] columns={"tile","expires"}; // =========================================================== // Constructors // =========================================================== /** * The tiles may be found on several media. This one works with tiles stored on the file system. * It and its friends are typically created and controlled by {@link MapTileProviderBase}. */ public MapTileSqlCacheProvider(final IRegisterReceiver pRegisterReceiver, final ITileSource pTileSource, final long pMaximumCachedFileAge) { super(pRegisterReceiver, Configuration.getInstance().getTileFileSystemThreads(), Configuration.getInstance().getTileFileSystemMaxQueueSize()); setTileSource(pTileSource); mMaximumCachedFileAge = pMaximumCachedFileAge; mWriter = new SqlTileWriter(); } public MapTileSqlCacheProvider(final IRegisterReceiver pRegisterReceiver, final ITileSource pTileSource) { this(pRegisterReceiver, pTileSource, OpenStreetMapTileProviderConstants.DEFAULT_MAXIMUM_CACHED_FILE_AGE); } // =========================================================== // Getter & Setter // =========================================================== // =========================================================== // Methods from SuperClass/Interfaces // =========================================================== @Override public boolean getUsesDataConnection() { return false; } @Override protected String getName() { return "SQL Cache Archive Provider"; } @Override protected String getThreadGroupName() { return "sqlcache"; } @Override protected Runnable getTileLoader() { return new TileLoader(); } @Override public int getMinimumZoomLevel() { ITileSource tileSource = mTileSource.get(); return tileSource != null ? tileSource.getMinimumZoomLevel() : OpenStreetMapTileProviderConstants.MINIMUM_ZOOMLEVEL; } @Override public int getMaximumZoomLevel() { ITileSource tileSource = mTileSource.get(); return tileSource != null ? tileSource.getMaximumZoomLevel() : microsoft.mappoint.TileSystem.getMaximumZoomLevel(); } @Override protected void onMediaMounted() { } @Override protected void onMediaUnmounted() { if (mWriter!=null) mWriter.onDetach(); mWriter=new SqlTileWriter(); } @Override public void setTileSource(final ITileSource pTileSource) { mTileSource.set(pTileSource); } @Override public void detach() { if (mWriter!=null) mWriter.onDetach(); mWriter=null; super.detach(); } // =========================================================== // Methods // =========================================================== /** * returns true if the given tile for the current map source exists in the cache db * @param pTile * @return */ public boolean hasTile(final MapTile pTile) { ITileSource tileSource = mTileSource.get(); if (tileSource == null) { return false; } final long x = (long) pTile.getX(); final long y = (long) pTile.getY(); final long z = (long) pTile.getZoomLevel(); final long index = ((z << z) + x << z) + y; final Cursor cur =mWriter.db.query(DatabaseFileArchive.TABLE,columns,"key = " + index + " and provider = '" + tileSource.name() + "'", null, null, null, null); if(cur.getCount() != 0) { cur.close(); return true; } return false; } // =========================================================== // Inner and Anonymous Classes // =========================================================== protected class TileLoader extends MapTileModuleProviderBase.TileLoader { @Override public Drawable loadTile(final MapTileRequestState pState) { ITileSource tileSource = mTileSource.get(); if (tileSource == null) { return null; } final MapTile pTile = pState.getMapTile(); // if there's no sdcard then don't do anything if (!isSdCardAvailable()) { if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"No sdcard - do nothing for tile: " + pTile); } return null; } if (mWriter==null || mWriter.db == null) { if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"Sqlwriter cache is offline - do nothing for tile: " + pTile); } return null; } InputStream inputStream = null; try { final long x = (long) pTile.getX(); final long y = (long) pTile.getY(); final long z = (long) pTile.getZoomLevel(); final long index = ((z << z) + x << z) + y; final Cursor cur =mWriter.db.query(DatabaseFileArchive.TABLE,columns,"key = " + index + " and provider = '" + tileSource.name() + "'", null, null, null, null); byte[] bits=null; long lastModified=0l; if(cur.getCount() != 0) { cur.moveToFirst(); bits = (cur.getBlob(cur.getColumnIndex("tile"))); lastModified = cur.getLong(cur.getColumnIndex("expires")); } cur.close(); if (bits==null) { if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"SqlCache - Tile doesn't exist: " +tileSource.name() + pTile); Counters.fileCacheMiss++; } return null; } inputStream = new ByteArrayInputStream(bits); Drawable drawable = tileSource.getDrawable(inputStream); // Check to see if file has expired final long now = System.currentTimeMillis(); final boolean fileExpired = lastModified < now - mMaximumCachedFileAge; if (fileExpired && drawable != null) { if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"Tile expired: " + tileSource.name() +pTile); } ExpirableBitmapDrawable.setDrawableExpired(drawable); //should we remove from the database here? } Counters.fileCacheHit++; return drawable; } catch (final Throwable e) { Log.e(IMapView.LOGTAG,"Error loading tile", e); } finally { if (inputStream != null) { StreamUtils.closeStream(inputStream); } } return null; } } }