package org.osmdroid.tileprovider.modules; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.Log; import org.osmdroid.api.IMapView; import org.osmdroid.config.Configuration; import org.osmdroid.tileprovider.BitmapPool; import org.osmdroid.tileprovider.MapTile; import org.osmdroid.tileprovider.MapTileRequestState; import org.osmdroid.tileprovider.ReusableBitmapDrawable; import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; import org.osmdroid.tileprovider.tilesource.BitmapTileSourceBase.LowMemoryException; import org.osmdroid.tileprovider.tilesource.ITileSource; import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase; import org.osmdroid.tileprovider.util.Counters; import org.osmdroid.tileprovider.util.StreamUtils; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.UnknownHostException; import java.util.Date; import java.util.concurrent.atomic.AtomicReference; /** * The {@link MapTileDownloader} loads tiles from an HTTP server. It saves downloaded tiles to an * IFilesystemCache if available. * * @author Marc Kurtz * @author Nicolas Gramlich * @author Manuel Stahl * */ public class MapTileDownloader extends MapTileModuleProviderBase { // =========================================================== // Constants // =========================================================== // =========================================================== // Fields // =========================================================== private final IFilesystemCache mFilesystemCache; private final AtomicReference<OnlineTileSourceBase> mTileSource = new AtomicReference<OnlineTileSourceBase>(); private final INetworkAvailablityCheck mNetworkAvailablityCheck; // =========================================================== // Constructors // =========================================================== public MapTileDownloader(final ITileSource pTileSource) { this(pTileSource, null, null); } public MapTileDownloader(final ITileSource pTileSource, final IFilesystemCache pFilesystemCache) { this(pTileSource, pFilesystemCache, null); } public MapTileDownloader(final ITileSource pTileSource, final IFilesystemCache pFilesystemCache, final INetworkAvailablityCheck pNetworkAvailablityCheck) { this(pTileSource, pFilesystemCache, pNetworkAvailablityCheck, Configuration.getInstance().getTileDownloadThreads(), Configuration.getInstance().getTileDownloadMaxQueueSize()); } public MapTileDownloader(final ITileSource pTileSource, final IFilesystemCache pFilesystemCache, final INetworkAvailablityCheck pNetworkAvailablityCheck, int pThreadPoolSize, int pPendingQueueSize) { super(pThreadPoolSize, pPendingQueueSize); mFilesystemCache = pFilesystemCache; mNetworkAvailablityCheck = pNetworkAvailablityCheck; setTileSource(pTileSource); } // =========================================================== // Getter & Setter // =========================================================== public ITileSource getTileSource() { return mTileSource.get(); } // =========================================================== // Methods from SuperClass/Interfaces // =========================================================== @Override public boolean getUsesDataConnection() { return true; } @Override protected String getName() { return "Online Tile Download Provider"; } @Override protected String getThreadGroupName() { return "downloader"; } @Override protected Runnable getTileLoader() { return new TileLoader(); } @Override public void detach() { super.detach(); if (this.mFilesystemCache!=null) this.mFilesystemCache.onDetach(); } @Override public int getMinimumZoomLevel() { OnlineTileSourceBase tileSource = mTileSource.get(); return (tileSource != null ? tileSource.getMinimumZoomLevel() : OpenStreetMapTileProviderConstants.MINIMUM_ZOOMLEVEL); } @Override public int getMaximumZoomLevel() { OnlineTileSourceBase tileSource = mTileSource.get(); return (tileSource != null ? tileSource.getMaximumZoomLevel() : microsoft.mappoint.TileSystem.getMaximumZoomLevel()); } @Override public void setTileSource(final ITileSource tileSource) { // We are only interested in OnlineTileSourceBase tile sources if (tileSource instanceof OnlineTileSourceBase) { mTileSource.set((OnlineTileSourceBase) tileSource); } else { // Otherwise shut down the tile downloader mTileSource.set(null); } } // =========================================================== // Inner and Anonymous Classes // =========================================================== protected class TileLoader extends MapTileModuleProviderBase.TileLoader { @Override public Drawable loadTile(final MapTileRequestState aState) throws CantContinueException { OnlineTileSourceBase tileSource = mTileSource.get(); if (tileSource == null) { return null; } InputStream in = null; OutputStream out = null; HttpURLConnection c=null; final MapTile tile = aState.getMapTile(); try { if (mNetworkAvailablityCheck != null && !mNetworkAvailablityCheck.getNetworkAvailable()) { if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"Skipping " + getName() + " due to NetworkAvailabliltyCheck."); } return null; } final String tileURLString = tileSource.getTileURLString(tile); if (Configuration.getInstance().isDebugMode()) { Log.d(IMapView.LOGTAG,"Downloading Maptile from url: " + tileURLString); } if (TextUtils.isEmpty(tileURLString)) { return null; } c = (HttpURLConnection) new URL(tileURLString).openConnection(); c.setUseCaches(true); c.setRequestProperty(Configuration.getInstance().getUserAgentHttpHeader(),Configuration.getInstance().getUserAgentValue()); c.connect(); // Check to see if we got success if (c.getResponseCode() != 200) { Log.w(IMapView.LOGTAG, "Problem downloading MapTile: " + tile + " HTTP response: " + c.getResponseMessage()); if (Configuration.getInstance().isDebugMapTileDownloader()) { Log.d(IMapView.LOGTAG, tileURLString); } Counters.tileDownloadErrors++; return null; } if (Configuration.getInstance().isDebugMapTileDownloader()) { Log.d(IMapView.LOGTAG, tileURLString + " success"); } in = c.getInputStream(); final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); out = new BufferedOutputStream(dataStream, StreamUtils.IO_BUFFER_SIZE); //default is 1 week from now Date dateExpires; Long override=Configuration.getInstance().getExpirationOverrideDuration(); if (override!=null) { dateExpires= new Date(System.currentTimeMillis() + override); } else { dateExpires = new Date(System.currentTimeMillis() + OpenStreetMapTileProviderConstants.DEFAULT_MAXIMUM_CACHED_FILE_AGE + Configuration.getInstance().getExpirationExtendedDuration()); final String expires = c.getHeaderField(OpenStreetMapTileProviderConstants.HTTP_EXPIRES_HEADER); if (expires != null && expires.length() > 0) { try { dateExpires = Configuration.getInstance().getHttpHeaderDateTimeFormat().parse(expires); dateExpires.setTime(dateExpires.getTime() + Configuration.getInstance().getExpirationExtendedDuration()); } catch (Exception ex) { if (Configuration.getInstance().isDebugMapTileDownloader()) Log.d(IMapView.LOGTAG, "Unable to parse expiration tag for tile, using default, server returned " + expires, ex); } } } tile.setExpires(dateExpires); StreamUtils.copy(in, out); out.flush(); final byte[] data = dataStream.toByteArray(); final ByteArrayInputStream byteStream = new ByteArrayInputStream(data); // Save the data to the cache //this is the only point in which we insert tiles to the db or local file system. if (mFilesystemCache != null) { mFilesystemCache.saveFile(tileSource, tile, byteStream); byteStream.reset(); } final Drawable result = tileSource.getDrawable(byteStream); return result; } catch (final UnknownHostException e) { // no network connection so empty the queue Log.w(IMapView.LOGTAG,"UnknownHostException downloading MapTile: " + tile + " : " + e); Counters.tileDownloadErrors++; throw new CantContinueException(e); } catch (final LowMemoryException e) { // low memory so empty the queue Counters.countOOM++; Log.w(IMapView.LOGTAG,"LowMemoryException downloading MapTile: " + tile + " : " + e); throw new CantContinueException(e); } catch (final FileNotFoundException e) { Counters.tileDownloadErrors++; Log.w(IMapView.LOGTAG,"Tile not found: " + tile + " : " + e); } catch (final IOException e) { Counters.tileDownloadErrors++; Log.w(IMapView.LOGTAG,"IOException downloading MapTile: " + tile + " : " + e); } catch (final Throwable e) { Counters.tileDownloadErrors++; Log.e(IMapView.LOGTAG,"Error downloading MapTile: " + tile, e); } finally { StreamUtils.closeStream(in); StreamUtils.closeStream(out); try{ c.disconnect(); } catch (Exception ex){} } return null; } @Override protected void tileLoaded(final MapTileRequestState pState, final Drawable pDrawable) { removeTileFromQueues(pState.getMapTile()); // don't return the tile because we'll wait for the fs provider to ask for it // this prevent flickering when a load of delayed downloads complete for tiles // that we might not even be interested in any more pState.getCallback().mapTileRequestCompleted(pState, null); // We want to return the Bitmap to the BitmapPool if applicable if (pDrawable instanceof ReusableBitmapDrawable) BitmapPool.getInstance().returnDrawableToPool((ReusableBitmapDrawable) pDrawable); } } }