package org.osmdroid.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.osmdroid.config.Configuration;
import org.osmdroid.tileprovider.ExpirableBitmapDrawable;
import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.MapTileRequestState;
import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import android.graphics.drawable.Drawable;
import android.util.Log;
import org.osmdroid.api.IMapView;
/**
* An abstract base class for modular tile providers
*
* @author Marc Kurtz
* @author Neil Boyd
*/
public abstract class MapTileModuleProviderBase {
/**
* 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;
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) {
Log.w(IMapView.LOGTAG,"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) {
// Make sure we're not detached
if (mExecutor.isShutdown())
return;
synchronized (mQueueLockObject) {
if (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"MapTileModuleProviderBase.loadMaptileAsync() on provider: "
+ getName() + " for tile: " + pState.getMapTile());
if (mPending.containsKey(pState.getMapTile()))
Log.d(IMapView.LOGTAG,"MapTileModuleProviderBase.loadMaptileAsync() tile already exists in request queue for modular provider. Moving to front of queue.");
else
Log.d(IMapView.LOGTAG,"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) {
Log.w(IMapView.LOGTAG,"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 (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"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 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 (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"TileLoader.nextTile() on provider: " + getName()
+ " found tile in working queue: " + tile);
}
result = tile;
}
}
if (result != null) {
if (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"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 (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"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 (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"TileLoader.tileLoadedExpired() on provider: " + getName()
+ " with tile: " + pState.getMapTile());
}
removeTileFromQueues(pState.getMapTile());
pState.getCallback().mapTileRequestExpiredTile(pState, pDrawable);
}
protected void tileLoadedFailed(final MapTileRequestState pState) {
if (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"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 (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"TileLoader.run() processing next tile: "
+ state.getMapTile()
+ ", pending:" + mPending.size()
+ ", working:" + mWorking.size()
);
}
try {
result = null;
result = loadTile(state);
} catch (final CantContinueException e) {
Log.i(IMapView.LOGTAG,"Tile loader can't continue: " + state.getMapTile(), e);
clearQueue();
} catch (final Throwable e) {
Log.i(IMapView.LOGTAG,"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);
}
}
}