// Created by plusminus on 21:46:22 - 25.09.2008 package org.osmdroid.tileprovider; import java.util.HashMap; import org.osmdroid.config.Configuration; import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; import org.osmdroid.tileprovider.modules.IFilesystemCache; import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase; import org.osmdroid.tileprovider.modules.TileWriter; import org.osmdroid.tileprovider.tilesource.ITileSource; import org.osmdroid.util.TileLooper; import org.osmdroid.views.Projection; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Handler; import android.util.Log; import org.osmdroid.api.IMapView; /** * This is an abstract class. The tile provider is responsible for: * <ul> * <li>determining if a map tile is available,</li> * <li>notifying the client, via a callback handler</li> * </ul> * see {@link MapTile} for an overview of how tiles are served by this provider. * * @author Marc Kurtz * @author Nicolas Gramlich * */ public abstract class MapTileProviderBase implements IMapTileProviderCallback { protected final MapTileCache mTileCache; protected Handler mTileRequestCompleteHandler; protected boolean mUseDataConnection = true; protected Drawable mTileNotFoundImage = null; private ITileSource mTileSource; /** * Attempts to get a Drawable that represents a {@link MapTile}. If the tile is not immediately * available this will return null and attempt to get the tile from known tile sources for * subsequent future requests. Note that this may return a {@link ReusableBitmapDrawable} in * which case you should follow proper handling procedures for using that Drawable or it may * reused while you are working with it. * * @see ReusableBitmapDrawable */ public abstract Drawable getMapTile(MapTile pTile); /** * classes that extend MapTileProviderBase must call this method to prevent memory leaks. * Updated 5.2+ */ public void detach(){ if (mTileNotFoundImage!=null){ // Only recycle if we are running on a project less than 2.3.3 Gingerbread. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { if (mTileNotFoundImage instanceof BitmapDrawable) { final Bitmap bitmap = ((BitmapDrawable) mTileNotFoundImage).getBitmap(); if (bitmap != null) { bitmap.recycle(); } } } if (mTileNotFoundImage instanceof ReusableBitmapDrawable) BitmapPool.getInstance().returnDrawableToPool((ReusableBitmapDrawable) mTileNotFoundImage); } mTileNotFoundImage=null; } /** * Gets the minimum zoom level this tile provider can provide * * @return the minimum zoom level */ public abstract int getMinimumZoomLevel(); /** * Gets the maximum zoom level this tile provider can provide * * @return the maximum zoom level */ public abstract int getMaximumZoomLevel(); /** * Sets the tile source for this tile provider. * * @param pTileSource * the tile source */ public void setTileSource(final ITileSource pTileSource) { mTileSource = pTileSource; clearTileCache(); } /** * Gets the tile source for this tile provider. * * @return the tile source */ public ITileSource getTileSource() { return mTileSource; } /** * Creates a {@link MapTileCache} to be used to cache tiles in memory. */ public MapTileCache createTileCache() { return new MapTileCache(); } public MapTileProviderBase(final ITileSource pTileSource) { this(pTileSource, null); } public MapTileProviderBase(final ITileSource pTileSource, final Handler pDownloadFinishedListener) { mTileCache = this.createTileCache(); mTileRequestCompleteHandler = pDownloadFinishedListener; mTileSource = pTileSource; } /** * Sets the "sorry we can't load a tile for this location" image. If it's null, the default view * is shown, which is the standard grey grid controlled by the tiles overlay * {@link org.osmdroid.views.overlay.TilesOverlay#setLoadingLineColor(int)} and * {@link org.osmdroid.views.overlay.TilesOverlay#setLoadingBackgroundColor(int)} * @since 5.2+ * @param drawable */ public void setTileLoadFailureImage(final Drawable drawable){ this.mTileNotFoundImage = drawable; } /** * Called by implementation class methods indicating that they have completed the request as * best it can. The tile is added to the cache, and a MAPTILE_SUCCESS_ID message is sent. * * @param pState * the map tile request state object * @param pDrawable * the Drawable of the map tile */ @Override public void mapTileRequestCompleted(final MapTileRequestState pState, final Drawable pDrawable) { // put the tile in the cache putTileIntoCache(pState, pDrawable); // tell our caller we've finished and it should update its view if (mTileRequestCompleteHandler != null) { mTileRequestCompleteHandler.sendEmptyMessage(MapTile.MAPTILE_SUCCESS_ID); } if (Configuration.getInstance().isDebugTileProviders()) { Log.d(IMapView.LOGTAG,"MapTileProviderBase.mapTileRequestCompleted(): " + pState.getMapTile()); } } /** * Called by implementation class methods indicating that they have failed to retrieve the * requested map tile. a MAPTILE_FAIL_ID message is sent. * * @param pState * the map tile request state object */ @Override public void mapTileRequestFailed(final MapTileRequestState pState) { if (mTileNotFoundImage!=null) { putTileIntoCache(pState, mTileNotFoundImage); if (mTileRequestCompleteHandler != null) { mTileRequestCompleteHandler.sendEmptyMessage(MapTile.MAPTILE_SUCCESS_ID); } } else { if (mTileRequestCompleteHandler != null) { mTileRequestCompleteHandler.sendEmptyMessage(MapTile.MAPTILE_FAIL_ID); } } if (Configuration.getInstance().isDebugTileProviders()) { Log.d(IMapView.LOGTAG,"MapTileProviderBase.mapTileRequestFailed(): " + pState.getMapTile()); } } /** * Called by implementation class methods indicating that they have produced an expired result * that can be used but better results may be delivered later. The tile is added to the cache, * and a MAPTILE_SUCCESS_ID message is sent. * * @param pState * the map tile request state object * @param pDrawable * the Drawable of the map tile */ @Override public void mapTileRequestExpiredTile(MapTileRequestState pState, Drawable pDrawable) { // Put the expired tile into the cache putExpiredTileIntoCache(pState, pDrawable); // tell our caller we've finished and it should update its view if (mTileRequestCompleteHandler != null) { mTileRequestCompleteHandler.sendEmptyMessage(MapTile.MAPTILE_SUCCESS_ID); } if (Configuration.getInstance().isDebugTileProviders()) { Log.d(IMapView.LOGTAG,"MapTileProviderBase.mapTileRequestExpiredTile(): " + pState.getMapTile()); } } protected void putTileIntoCache(MapTileRequestState pState, Drawable pDrawable) { final MapTile tile = pState.getMapTile(); if (pDrawable != null) { mTileCache.putTile(tile, pDrawable); } } protected void putExpiredTileIntoCache(MapTileRequestState pState, Drawable pDrawable) { final MapTile tile = pState.getMapTile(); if (pDrawable != null && !mTileCache.containsTile(tile)) { mTileCache.putTile(tile, pDrawable); } } public void setTileRequestCompleteHandler(final Handler handler) { mTileRequestCompleteHandler = handler; } public void ensureCapacity(final int pCapacity) { mTileCache.ensureCapacity(pCapacity); } /** * purges the cache of all tiles (default is the in memory cache) */ public void clearTileCache() { mTileCache.clear(); } /** * Whether to use the network connection if it's available. */ @Override public boolean useDataConnection() { return mUseDataConnection; } /** * Set whether to use the network connection if it's available. * * @param pMode * if true use the network connection if it's available. if false don't use the * network connection even if it's available. */ public void setUseDataConnection(final boolean pMode) { mUseDataConnection = pMode; } /** * Recreate the cache using scaled versions of the tiles currently in it * @param pNewZoomLevel the zoom level that we need now * @param pOldZoomLevel the previous zoom level that we should get the tiles to rescale * @param pViewPort the view port we need tiles for */ public void rescaleCache(final Projection pProjection, final int pNewZoomLevel, final int pOldZoomLevel, final Rect pViewPort) { if (pNewZoomLevel == pOldZoomLevel) { return; } final long startMs = System.currentTimeMillis(); Log.i(IMapView.LOGTAG,"rescale tile cache from "+ pOldZoomLevel + " to " + pNewZoomLevel); final int tileSize = getTileSource().getTileSizePixels(); Point topLeftMercator = pProjection.toMercatorPixels(pViewPort.left, pViewPort.top, null); Point bottomRightMercator = pProjection.toMercatorPixels(pViewPort.right, pViewPort.bottom, null); final Rect viewPort = new Rect(topLeftMercator.x, topLeftMercator.y, bottomRightMercator.x, bottomRightMercator.y); final ScaleTileLooper tileLooper = pNewZoomLevel > pOldZoomLevel ? new ZoomInTileLooper(pOldZoomLevel) : new ZoomOutTileLooper(pOldZoomLevel); tileLooper.loop(null, pNewZoomLevel, tileSize, viewPort); final long endMs = System.currentTimeMillis(); Log.i(IMapView.LOGTAG,"Finished rescale in " + (endMs - startMs) + "ms"); } private abstract class ScaleTileLooper extends TileLooper { /** new (scaled) tiles to add to cache * NB first generate all and then put all in cache, * otherwise the ones we need will be pushed out */ protected final HashMap<MapTile, Bitmap> mNewTiles; protected final int mOldZoomLevel; protected int mDiff; protected int mTileSize_2; protected Rect mSrcRect; protected Rect mDestRect; protected Paint mDebugPaint; public ScaleTileLooper(final int pOldZoomLevel) { mOldZoomLevel = pOldZoomLevel; mNewTiles = new HashMap<MapTile, Bitmap>(); mSrcRect = new Rect(); mDestRect = new Rect(); mDebugPaint = new Paint(); } @Override public void initialiseLoop(final int pZoomLevel, final int pTileSizePx) { mDiff = Math.abs(pZoomLevel - mOldZoomLevel); mTileSize_2 = pTileSizePx >> mDiff; } @Override public void handleTile(final Canvas pCanvas, final int pTileSizePx, final MapTile pTile, final int pX, final int pY) { // Get tile from cache. // If it's found then no need to created scaled version. // If not found (null) them we've initiated a new request for it, // and now we'll create a scaled version until the request completes. final Drawable requestedTile = getMapTile(pTile); if (requestedTile == null) { try { handleTile(pTileSizePx, pTile, pX, pY); } catch(final OutOfMemoryError e) { Log.e(IMapView.LOGTAG,"OutOfMemoryError rescaling cache"); } } } @Override public void finaliseLoop() { // now add the new ones, pushing out the old ones while (!mNewTiles.isEmpty()) { final MapTile tile = mNewTiles.keySet().iterator().next(); final Bitmap bitmap = mNewTiles.remove(tile); final ExpirableBitmapDrawable drawable = new ReusableBitmapDrawable(bitmap); ExpirableBitmapDrawable.setDrawableExpired(drawable); final Drawable existingTile = mTileCache.getMapTile(tile); if (existingTile == null || ExpirableBitmapDrawable.isDrawableExpired(existingTile)) putExpiredTileIntoCache(new MapTileRequestState(tile, new MapTileModuleProviderBase[0], null), drawable); } } protected abstract void handleTile(int pTileSizePx, MapTile pTile, int pX, int pY); } private class ZoomInTileLooper extends ScaleTileLooper { public ZoomInTileLooper(final int pOldZoomLevel) { super(pOldZoomLevel); } @Override public void handleTile(final int pTileSizePx, final MapTile pTile, final int pX, final int pY) { // get the correct fraction of the tile from cache and scale up final MapTile oldTile = new MapTile(mOldZoomLevel, pTile.getX() >> mDiff, pTile.getY() >> mDiff); final Drawable oldDrawable = mTileCache.getMapTile(oldTile); if (oldDrawable instanceof BitmapDrawable) { final int xx = (pX % (1 << mDiff)) * mTileSize_2; final int yy = (pY % (1 << mDiff)) * mTileSize_2; mSrcRect.set(xx, yy, xx + mTileSize_2, yy + mTileSize_2); mDestRect.set(0, 0, pTileSizePx, pTileSizePx); // Try to get a bitmap from the pool, otherwise allocate a new one Bitmap bitmap = BitmapPool.getInstance().obtainSizedBitmapFromPool( pTileSizePx, pTileSizePx); if (bitmap == null) bitmap = Bitmap.createBitmap(pTileSizePx, pTileSizePx, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); final boolean isReusable = oldDrawable instanceof ReusableBitmapDrawable; final ReusableBitmapDrawable reusableBitmapDrawable = isReusable ? (ReusableBitmapDrawable) oldDrawable : null; boolean success = false; if (isReusable) reusableBitmapDrawable.beginUsingDrawable(); try { if (!isReusable || reusableBitmapDrawable.isBitmapValid()) { final BitmapDrawable bitmapDrawable = (BitmapDrawable) oldDrawable; final Bitmap oldBitmap = bitmapDrawable.getBitmap(); canvas.drawBitmap(oldBitmap, mSrcRect, mDestRect, null); success = true; if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"Created scaled tile: " + pTile); mDebugPaint.setTextSize(40); canvas.drawText("scaled", 50, 50, mDebugPaint); } } } finally { if (isReusable) reusableBitmapDrawable.finishUsingDrawable(); } if (success) mNewTiles.put(pTile, bitmap); } } } private class ZoomOutTileLooper extends ScaleTileLooper { private static final int MAX_ZOOM_OUT_DIFF = 4; public ZoomOutTileLooper(final int pOldZoomLevel) { super(pOldZoomLevel); } @Override protected void handleTile(final int pTileSizePx, final MapTile pTile, final int pX, final int pY) { if (mDiff >= MAX_ZOOM_OUT_DIFF){ return; } // get many tiles from cache and make one tile from them final int xx = pTile.getX() << mDiff; final int yy = pTile.getY() << mDiff; final int numTiles = 1 << mDiff; Bitmap bitmap = null; Canvas canvas = null; for(int x = 0; x < numTiles; x++) { for(int y = 0; y < numTiles; y++) { final MapTile oldTile = new MapTile(mOldZoomLevel, xx + x, yy + y); final Drawable oldDrawable = mTileCache.getMapTile(oldTile); if (oldDrawable instanceof BitmapDrawable) { final Bitmap oldBitmap = ((BitmapDrawable)oldDrawable).getBitmap(); if (oldBitmap != null) { if (bitmap == null) { // Try to get a bitmap from the pool, otherwise allocate a new one bitmap = BitmapPool.getInstance().obtainSizedBitmapFromPool( pTileSizePx, pTileSizePx); if (bitmap == null) bitmap = Bitmap.createBitmap(pTileSizePx, pTileSizePx, Bitmap.Config.ARGB_8888); canvas = new Canvas(bitmap); canvas.drawColor(Color.LTGRAY); } mDestRect.set( x * mTileSize_2, y * mTileSize_2, (x + 1) * mTileSize_2, (y + 1) * mTileSize_2); if (oldBitmap != null) { canvas.drawBitmap(oldBitmap, null, mDestRect, null); mTileCache.mCachedTiles.remove(oldBitmap); } } } } } if (bitmap != null) { mNewTiles.put(pTile, bitmap); if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"Created scaled tile: " + pTile); mDebugPaint.setTextSize(40); canvas.drawText("scaled", 50, 50, mDebugPaint); } } } } public abstract IFilesystemCache getTileWriter(); /** * @since 5.6 * @return the number of tile requests currently in the queue */ public abstract long getQueueSize(); }