package com.mutu.mapapi.tileprovider.modules; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mutu.mapapi.tileprovider.ExpirableBitmapDrawable; import com.mutu.mapapi.tileprovider.MapTile; import com.mutu.mapapi.tileprovider.MapTileRequestState; import com.mutu.mapapi.tileprovider.constants.OpenStreetMapTileProviderConstants; import com.mutu.mapapi.tileprovider.tilesource.ITileSource; import android.graphics.drawable.Drawable; /** * An abstract base class for modular tile providers * * @author Marc Kurtz * @author Neil Boyd */ public abstract class MapTileModuleProviderBase implements OpenStreetMapTileProviderConstants { /** * Gets the human-friendly name assigned to this tile provider. * * @return the thread name */ protected abstract String getName(); /** * Gets the name assigned to the thread for this provider. * * @return the thread name */ protected abstract String getThreadGroupName(); /** * It is expected that the implementation will construct an internal member which internally * implements a {@link TileLoader}. This method is expected to return a that internal member to * methods of the parent methods. * * @return the internal member of this tile provider. */ protected abstract Runnable getTileLoader(); /** * Returns true if implementation uses a data connection, false otherwise. This value is used to * determine if this provider should be skipped if there is no data connection. * * @return true if implementation uses a data connection, false otherwise */ public abstract boolean getUsesDataConnection(); /** * 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 tileSource * the tile source */ public abstract void setTileSource(ITileSource tileSource); private final ExecutorService mExecutor; private static final Logger logger = LoggerFactory.getLogger(MapTileModuleProviderBase.class); protected final Object mQueueLockObject = new Object(); protected final HashMap<MapTile, MapTileRequestState> mWorking; protected final LinkedHashMap<MapTile, MapTileRequestState> mPending; public MapTileModuleProviderBase(int pThreadPoolSize, final int pPendingQueueSize) { if (pPendingQueueSize < pThreadPoolSize) { logger.warn("The pending queue size is smaller than the thread pool size. Automatically reducing the thread pool size."); pThreadPoolSize = pPendingQueueSize; } mExecutor = Executors.newFixedThreadPool(pThreadPoolSize, new ConfigurablePriorityThreadFactory(Thread.NORM_PRIORITY, getThreadGroupName())); mWorking = new HashMap<MapTile, MapTileRequestState>(); mPending = new LinkedHashMap<MapTile, MapTileRequestState>(pPendingQueueSize + 2, 0.1f, true) { private static final long serialVersionUID = 6455337315681858866L; @Override protected boolean removeEldestEntry( final Map.Entry<MapTile, MapTileRequestState> pEldest) { if (size() > pPendingQueueSize) { MapTile result = null; // get the oldest tile that isn't in the mWorking queue Iterator<MapTile> iterator = mPending.keySet().iterator(); while (result == null && iterator.hasNext()) { final MapTile tile = iterator.next(); if (!mWorking.containsKey(tile)) { result = tile; } } if (result != null) { MapTileRequestState state = mPending.get(result); removeTileFromQueues(result); state.getCallback().mapTileRequestFailed(state); } } return false; } }; } public void loadMapTileAsync(final MapTileRequestState pState) { synchronized (mQueueLockObject) { if (DEBUG_TILE_PROVIDERS) { logger.debug("MapTileModuleProviderBase.loadMaptileAsync() on provider: " + getName() + " for tile: " + pState.getMapTile()); if (mPending.containsKey(pState.getMapTile())) logger.debug("MapTileModuleProviderBase.loadMaptileAsync() tile already exists in request queue for modular provider. Moving to front of queue."); else logger.debug("MapTileModuleProviderBase.loadMaptileAsync() adding tile to request queue for modular provider."); } // this will put the tile in the queue, or move it to the front of // the queue if it's already present mPending.put(pState.getMapTile(), pState); } try { mExecutor.execute(getTileLoader()); } catch (final RejectedExecutionException e) { logger.warn("RejectedExecutionException", e); } } private void clearQueue() { synchronized (mQueueLockObject) { mPending.clear(); mWorking.clear(); } } /** * Detach, we're shutting down - Stops all workers. */ public void detach() { this.clearQueue(); this.mExecutor.shutdown(); } void removeTileFromQueues(final MapTile mapTile) { synchronized (mQueueLockObject) { if (DEBUG_TILE_PROVIDERS) { logger.debug("MapTileModuleProviderBase.removeTileFromQueues() on provider: " + getName() + " for tile: " + mapTile); } mPending.remove(mapTile); mWorking.remove(mapTile); } } /** * Load the requested tile. An abstract internal class whose objects are used by worker threads * to acquire tiles from servers. It processes tiles from the 'pending' set to the 'working' set * as they become available. The key unimplemented method is 'loadTile'. */ protected abstract class TileLoader implements Runnable { /** * Load the requested tile. * * @return the tile if it was loaded successfully, or null if failed to * load and other tile providers need to be called * @param pState * @throws {@link CantContinueException} */ protected abstract Drawable loadTile(MapTileRequestState pState) throws CantContinueException; protected void onTileLoaderInit() { // Do nothing by default } protected void onTileLoaderShutdown() { // Do nothing by default } protected MapTileRequestState nextTile() { synchronized (mQueueLockObject) { MapTile result = null; // get the most recently accessed tile // - the last item in the iterator that's not already being // processed Iterator<MapTile> iterator = mPending.keySet().iterator(); // TODO this iterates the whole list, make this faster... while (iterator.hasNext()) { final MapTile tile = iterator.next(); if (!mWorking.containsKey(tile)) { if (DEBUG_TILE_PROVIDERS) { logger.debug("TileLoader.nextTile() on provider: " + getName() + " found tile in working queue: " + tile); } result = tile; } } if (result != null) { if (DEBUG_TILE_PROVIDERS) { logger.debug("TileLoader.nextTile() on provider: " + getName() + " adding tile to working queue: " + result); } mWorking.put(result, mPending.get(result)); } return (result != null ? mPending.get(result) : null); } } /** * A tile has loaded. */ protected void tileLoaded(final MapTileRequestState pState, final Drawable pDrawable) { if (DEBUG_TILE_PROVIDERS) { logger.debug("TileLoader.tileLoaded() on provider: " + getName() + " with tile: " + pState.getMapTile()); } removeTileFromQueues(pState.getMapTile()); pState.getCallback().mapTileRequestCompleted(pState, pDrawable); } /** * A tile has loaded but it's expired. * Return it <b>and</b> send request to next provider. */ protected void tileLoadedExpired(final MapTileRequestState pState, final Drawable pDrawable) { if (DEBUG_TILE_PROVIDERS) { logger.debug("TileLoader.tileLoadedExpired() on provider: " + getName() + " with tile: " + pState.getMapTile()); } removeTileFromQueues(pState.getMapTile()); pState.getCallback().mapTileRequestExpiredTile(pState, pDrawable); } protected void tileLoadedFailed(final MapTileRequestState pState) { if (DEBUG_TILE_PROVIDERS) { logger.debug("TileLoader.tileLoadedFailed() on provider: " + getName() + " with tile: " + pState.getMapTile()); } removeTileFromQueues(pState.getMapTile()); pState.getCallback().mapTileRequestFailed(pState); } /** * This is a functor class of type Runnable. The run method is the encapsulated function. */ @Override final public void run() { onTileLoaderInit(); MapTileRequestState state; Drawable result = null; while ((state = nextTile()) != null) { if (DEBUG_TILE_PROVIDERS) { logger.debug("TileLoader.run() processing next tile: " + state.getMapTile()); } try { result = null; result = loadTile(state); } catch (final CantContinueException e) { logger.info("Tile loader can't continue: " + state.getMapTile(), e); clearQueue(); } catch (final Throwable e) { logger.error("Error downloading tile: " + state.getMapTile(), e); } if (result == null) { tileLoadedFailed(state); } else if (ExpirableBitmapDrawable.isDrawableExpired(result)) { tileLoadedExpired(state, result); } else { tileLoaded(state, result); } } onTileLoaderShutdown(); } } /** * Thrown by a tile provider module in TileLoader.loadTile() to signal that it can no longer * function properly. This will typically clear the pending queue. */ public class CantContinueException extends Exception { private static final long serialVersionUID = 146526524087765133L; public CantContinueException(final String pDetailMessage) { super(pDetailMessage); } public CantContinueException(final Throwable pThrowable) { super(pThrowable); } } }