package com.androidol.util.tiles;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.HashSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import com.androidol.constants.UtilConstants;
import com.androidol.events.Event;
import com.androidol.events.TileEvents;
import com.androidol.exceptions.EmptyCacheException;
import com.androidol.exceptions.TileFileCorruptedException;
import com.androidol.tile.Tile;
import com.androidol.util.Util;
import com.vividsolutions.jts.geom.Coordinate;
public class TileSDCardFutureLoader extends TileFileSystemLoader implements UtilConstants {
protected static final int CacheSizeInByteOnSDCard = 512*1024*1024;
protected boolean mergeContextCacheToSDCard = true;
protected String rootOutputPath = "unionstation"; // just a joke
/**
* Constructor TileFileSystemFutureLoader
*
* @param context
* @param cacheSizeInByte
* @param tileCache
* @param tileEvents
*/
public TileSDCardFutureLoader(Context context, final int cacheSizeInByte, final TileCache tileCache, TileEvents tileEvents) {
super(context, cacheSizeInByte, tileCache, tileEvents);
}
/**
*
* @param url
* @param tile
* @return
* @throws FileNotFoundException
* @throws TileFileCorruptedException
*/
public Future<?> loadTileToMemoryAsync(final String url, final String signature, final Tile tile) throws FileNotFoundException, TileFileCorruptedException {
if(this.pendingQueue.contains(url)) {
//Util.printDebugMessage("...tile " + url + " already in the queue...skip loading from disk....");
// TODO: should I kill the previous loading thread and start a new one?
//this.pendingQueue.remove(url);
return null;
}
// determine whether the tile file exists on FS, or if so, is it corrupted
FileInputStream fileInputStream = null;
// cached tiles are stores in SD card
final String formattedUrlString = formatTileUrlToTileFilePath(url);
try {
// cached tiles are stores in SD card
fileInputStream = new FileInputStream(new File(formattedUrlString));
// TODO: check file existence
if(fileInputStream.available() == 0) {
Util.printDebugMessage(" ...tile " + url + " may be corrupted on disk...");
//this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url));
throw new TileFileCorruptedException("...load tile " + url + " from disk failed...file corrupted...");
}
} catch(FileNotFoundException e) {
throw e; // throw FileNotFoundException to TileProvider to trigger HTTP loading
} catch(IOException e) {
throw new TileFileCorruptedException("...load tile " + url + " from disk failed...file corrupted...");
} finally {
// TODO: delete this entry from disk so that -
// - the follow on http loader will get it again and fill the gap
}
final InputStream in = new BufferedInputStream(fileInputStream, IO_BUFFER_SIZE);
synchronized(this) {
this.pendingQueue.add(url);
}
Future<?> future = this.threadPool.submit(
new Runnable() {
@Override
public void run() {
//Util.printDebugMessage("@...started a thread for fs loading...signature: " + signature);
OutputStream out = null;
try {
// TODO: no need
//TileSDCardFutureLoader.this.database.incrementUse(formattedUrlString);
final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
out = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE);
StreamUtils.copy(in, out);
out.flush();
final byte[] data = dataStream.toByteArray();
//Util.printDebugMessage("...bitmap size in bytes: " + data.length + "...");
if(data.length <= 0) {
Util.printWarningMessage("...load tile " + url + " from disk failed...file corrupted...");
//TileFileSystemLoader.this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url));
TileSDCardFutureLoader.this.tileCache.removeTile(url);
TileSDCardFutureLoader.this.database.removeTile(formatTileUrlToTileFilePath(url));
// TODO: update the this.cacheUsedInByte
throw new TileFileCorruptedException("...load tile " + url + " from disk failed...file corrupted...");
}
final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
//Util.printDebugMessage(" ...image size: " + data.length + " bytes");
TileSDCardFutureLoader.this.tileCache.putTile(url, bitmap, tile);
//TileFileSystemLoader.this.pendingQueue.remove(url);
//Util.printDebugMessage("...fs loader pending queue size: " + TileFileSystemLoader.this.pendingQueue.size());
TileSDCardFutureLoader.this.events.triggerEvent(TileEvents.FS_LOAD_SUCCESS, new Event(TileEvents.FS_LOAD_SUCCESS, url));
//Util.printDebugMessage("...tile loaded in memory: " + url + "...");
} catch(Exception e) {
//TileFileSystemFutureLoader.this.pendingQueue.remove(url);
//TileFileSystemLoader.this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url));
// TODO: exception caught here can not be throw out to main thread in TileProvider
Util.printErrorMessage("...load tile " + url + " from disk failed...file corrupted...");
} finally {
StreamUtils.closeStream(in);
StreamUtils.closeStream(out);
TileSDCardFutureLoader.this.pendingQueue.remove(url);
//Util.printDebugMessage("@...finish a thread for fs loading...signature: " + signature);
}
//Util.printDebugMessage(" ...tile " + url + " removed from TileFileSystemLoader pending queue...queue size: " + TileFileSystemLoader.this.pendingQueue.size());
//Util.printDebugMessage(" ...TileFileSystemLoader pending queue size: " + TileFileSystemLoader.this.pendingQueue.size());
}
}
);
return future;
}
/**
* API Method: saveFile
*
* @param url
* @param data
* @throws IOException
*/
@Override
public void saveFile(final String url, final byte[] data) throws IOException {
// store cached tiles on SD card
final String fileName = formatTileUrlToTileFilePath(url);
File tileFile = new File(fileName);
// TODO: check status of SD card to see whether it is writable
// TODO: be more careful on folder handling
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
if(tileFile.getParentFile().exists() == false) {
tileFile.getParentFile().mkdirs();
}
fos = new FileOutputStream(tileFile);
bos = new BufferedOutputStream(fos, StreamUtils.IO_BUFFER_SIZE);
bos.write(data);
bos.flush();
} catch(Exception e) {
Util.printDebugMessage(e.getClass().getName());
Util.printDebugMessage("...error saving tiles on SD card..." + e.getMessage());
} finally {
bos.close();
}
synchronized(this) {
final int bytesGrown = this.database.addTileOrIncrement(fileName, data.length);
this.cacheUsedInByte += bytesGrown;
//Util.printDebugMessage("used cache is now: " + this.cacheUsedInByte + " bytes...");
try {
// TODO: SD card obviously has a much bigger cache than context
if(this.cacheUsedInByte > CacheSizeInByteOnSDCard){ // SD card cache: 512M bytes
//Util.printDebugMessage(" ...used cache exceeds max cache size...free cache...");
this.cacheUsedInByte -= this.database.deleteOldest((int)(this.cacheSizeInByte * 0.05f)); // Free 5% of cache
}
//printCurrentFileSystemCacheStatus();
} catch(EmptyCacheException e) {
Util.printErrorMessage("...cache empty...", e);
}
}
}
/**
*
*/
@Override
protected String formatTileUrlToTileFilePath(String url) {
String tileFilePath = "";
String basePath = "";
String externalStoreDir = Environment.getExternalStorageDirectory().getAbsolutePath();
if(externalStoreDir!=null && "".equalsIgnoreCase(externalStoreDir)==false) {
basePath = externalStoreDir + "/androidol/packages/" + this.rootOutputPath;
} else {
basePath = "/sdcard/androidol/packages/" + this.rootOutputPath;
}
String httpStripped = url.substring(7);
String folderName = httpStripped.substring(0, httpStripped.indexOf("/"));
String fileName = httpStripped.substring(httpStripped.indexOf("/")+1, httpStripped.length()).replace('/', '_');
// !!!TODO: this is just a temporary workaround
if(url.indexOf("WMS")>=0 || url.indexOf("Export")>=0) {
// TODO: figure out a better algorithm to encode url to file name for WMS request
fileName = fileName.substring(fileName.indexOf("BBOX=")+5, fileName.indexOf("&BGCOLOR"));
fileName = fileName.replace(',', '_');
fileName = fileName.replace('-', '_');
fileName = fileName + ".png";
}
tileFilePath = basePath + "/" + folderName + "/" + fileName;
return tileFilePath;
}
// ==================================================================================================
// getters & setters
// ==================================================================================================
public String getRootOutputPath() {
return rootOutputPath;
}
public void setRootOutputPath(String rootOutputPath) {
this.rootOutputPath = rootOutputPath;
}
}