package org.mozilla.osmdroid.tileprovider; import android.graphics.drawable.Drawable; import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil; import org.mozilla.osmdroid.tileprovider.modules.MapTileModuleProviderBase; import org.mozilla.osmdroid.tileprovider.tilesource.ITileSource; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This top-level tile provider allows a consumer to provide an array of modular asynchronous tile * providers to be used to obtain map tiles. When a tile is requested, the * {@link BetterMapTileProviderArray} first checks the {@link MapTileCache} (synchronously) and returns * the tile if available. If not, then the {@link BetterMapTileProviderArray} returns null and sends the * tile request through the asynchronous tile request chain. Each asynchronous tile provider returns * success/failure to the {@link BetterMapTileProviderArray}. If successful, the * {@link BetterMapTileProviderArray} passes the result to the base class. If failed, then the next * asynchronous tile provider is called in the chain. If there are no more asynchronous tile * providers in the chain, then the failure result is passed to the base class. The * {@link BetterMapTileProviderArray} provides a mechanism so that only one unique tile-request can be in * the map tile request chain at a time. * * @author Marc Kurtz */ public class BetterMapTileProviderArray extends MapTileProviderBase { private static final String LOG_TAG = LoggerUtil.makeLogTag(BetterMapTileProviderArray.class); protected final Map<MapTile, MapTileRequestState> mWorking = Collections.synchronizedMap(new HashMap<MapTile, MapTileRequestState>()); protected final List<MapTileModuleProviderBase> mTileProviderList; /** * Creates an {@link BetterMapTileProviderArray} with no tile providers. * * @param pRegisterReceiver a {@link IRegisterReceiver} */ protected BetterMapTileProviderArray(final ITileSource pTileSource, final IRegisterReceiver pRegisterReceiver) { this(pTileSource, pRegisterReceiver, new MapTileModuleProviderBase[0]); } /** * Creates an {@link BetterMapTileProviderArray} with the specified tile providers. * * @param aRegisterReceiver a {@link IRegisterReceiver} * @param pTileProviderArray an array of {@link MapTileModuleProviderBase} */ public BetterMapTileProviderArray(final ITileSource pTileSource, final IRegisterReceiver aRegisterReceiver, final MapTileModuleProviderBase[] pTileProviderArray) { super(pTileSource); mTileProviderList = new ArrayList<MapTileModuleProviderBase>(); Collections.addAll(mTileProviderList, pTileProviderArray); } @Override public synchronized void detach() { for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { tileProvider.detach(); } mWorking.clear(); } @Override public synchronized Drawable getMapTile(final MapTile pTile) { final Drawable tile = mTileCache.getMapTile(pTile); if (tile != null) { return tile; } // @TODO: vng - rewrite this whole thing, we need to create a // MapTileRequest and pass it through the chain of providers. // We need to manage a synchronized set of requests which are // in a request state. // // Any call to getMapTile that misses the cache, or is a // duplicate request for a tile that is already enqued will // yield a null. boolean alreadyInProgress = false; alreadyInProgress = mWorking.containsKey(pTile); if (!alreadyInProgress) { final MapTileModuleProviderBase[] providerArray = new MapTileModuleProviderBase[mTileProviderList.size()]; // Creat a MapTileRequestState that has a pointer to // an array of providers final MapTileRequestState state = new MapTileRequestState(pTile, mTileProviderList.toArray(providerArray), this); mWorking.put(pTile, state); //long ts = System.currentTimeMillis(); //Log.i(LOG_TAG, "This: ["+this+"] " + ts + " mWorking Put: ["+pTile+"]"); final MapTileModuleProviderBase provider = findNextAppropriateProvider(state); if (provider != null) { // @TODO: vng loadMapTileAsync spawns a thread to download // the tile, I think this is racy as multiple async calls seem // to be happening on the same URL provider.loadMapTileAsync(state); } else { mapTileRequestFailed(state); } } else { //long ts = System.currentTimeMillis(); //Log.i(LOG_TAG, ts +" mWorking is processing: ["+pTile+"]"); } return tile; } @Override public synchronized void mapTileRequestCompleted(final MapTileRequestState aState, final Drawable aDrawable) { mWorking.remove(aState.getMapTile()); //Log.i(LOG_TAG, "This: ["+this+"] mWorking Remove: ["+aState.getMapTile()+"]"); super.mapTileRequestCompleted(aState, aDrawable); } @Override public synchronized void mapTileRequestFailed(final MapTileRequestState aState) { final MapTileModuleProviderBase nextProvider = findNextAppropriateProvider(aState); if (nextProvider != null) { nextProvider.loadMapTileAsync(aState); } else { mWorking.remove(aState.getMapTile()); //Log.i(LOG_TAG, "This: ["+this+"] mWorking Remove: ["+aState.getMapTile()+"]"); super.mapTileRequestFailed(aState); } } @Override public synchronized void mapTileRequestExpiredTile(MapTileRequestState aState, Drawable aDrawable) { // Call through to the super first so aState.getCurrentProvider() still contains the proper // provider. super.mapTileRequestExpiredTile(aState, aDrawable); // Continue through the provider chain final MapTileModuleProviderBase nextProvider = findNextAppropriateProvider(aState); if (nextProvider != null) { nextProvider.loadMapTileAsync(aState); } else { mWorking.remove(aState.getMapTile()); //Log.i(LOG_TAG, "This: ["+this+"] mWorking Remove: ["+aState.getMapTile()+"]"); } } /** * We want to not use a provider that doesn't exist anymore in the chain, and we want to not use * a provider that requires a data connection when one is not available. */ private MapTileModuleProviderBase findNextAppropriateProvider(final MapTileRequestState aState) { MapTileModuleProviderBase provider = null; boolean providerDoesntExist = false, providerCantGetDataConnection = false, providerCantServiceZoomlevel = false; // The logic of the while statement is // "Keep looping until you get null, or a provider that still exists // and has a data connection if it needs one and can service the zoom level," do { provider = aState.getNextProvider(); // Perform some checks to see if we can use this provider // If any of these are true, then that disqualifies the provider for this tile request. if (provider != null) { providerDoesntExist = !this.getProviderExists(provider); providerCantGetDataConnection = !useDataConnection() && provider.getUsesDataConnection(); int zoomLevel = aState.getMapTile().getZoomLevel(); providerCantServiceZoomlevel = zoomLevel > provider.getMaximumZoomLevel() || zoomLevel < provider.getMinimumZoomLevel(); } } while ((provider != null) && (providerDoesntExist || providerCantGetDataConnection || providerCantServiceZoomlevel)); return provider; } public synchronized boolean getProviderExists(final MapTileModuleProviderBase provider) { return mTileProviderList.contains(provider); } @Override public synchronized int getMinimumZoomLevel() { int result = MAXIMUM_ZOOMLEVEL; for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { if (tileProvider.getMinimumZoomLevel() < result) { result = tileProvider.getMinimumZoomLevel(); } } return result; } @Override public synchronized int getMaximumZoomLevel() { int result = MINIMUM_ZOOMLEVEL; for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { if (tileProvider.getMaximumZoomLevel() > result) { result = tileProvider.getMaximumZoomLevel(); } } return result; } @Override public synchronized void setTileSource(final ITileSource aTileSource) { super.setTileSource(aTileSource); for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { tileProvider.setTileSource(aTileSource); clearTileCache(); } } }