www.pudn.com > GMapViewer-src.zip > MapTileCache.java
package org.sreid.j2me.gmapviewer;
import java.io.*;
import java.util.*;
import javax.microedition.rms.*;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
import org.sreid.j2me.util.*;
class MapTileCache {
private static final byte CACHE_VERSION = 1;
private static final String RECORD_STORE_NAME = "GMapViewer.tilecache";
private static final int CACHE_INDEX_RECORD_ID = 1;
private static final int KILO = 1024; // debatable if this should be 1000 or 1024
private final GMapViewer app;
private final LinkedHashtable cache; // XYZ:MapTile (last is most recently used)
// XXX Should I hold rms open (as I do now) or open/use/close repeatedly?
// getSizeAvailable() seems inaccurate with current approach (in emulator at least)
// which results in overzealous deletes.
private RecordStore rms = null;
int imageCacheSize; // How many decompressed Image objects to keep. Set by GMapCanvas.
private final Vector trash = new Vector(); // used only by cacheMaintenance, otherwise empty
// Cache info
private int memUsed = 0, rmsUsed = 0;
private int memCount = 0, rmsCount = 0;
MapTileCache(GMapViewer app) {
this.app = app;
XYZ xyz = new XYZ(0,0,0);
cache = new LinkedHashtable(xyz.getClass(), new MapTile(xyz).getClass());
}
synchronized MapTile get(XYZ xyz) {
MapTile mt = (MapTile)cache.get(xyz);
if (mt == null) return null;
keep(mt);
return mt;
}
synchronized void put(MapTile mt) {
keep(mt);
cacheMaintenance();
}
synchronized void loadFromRMS(MapTile mt) {
if (mt.bytes != null || mt.rmsID == -1 || rms == null) return;
// Load it from the RMS cache.
try {
byte[] record = rms.getRecord(mt.rmsID);
ByteArrayInputStream bais = new ByteArrayInputStream(record);
DataInputStream din = new DataInputStream(bais);
MapTile tmp = new MapTile(din, MapTile.WITH_BYTES);
din.close();
bais.close();
if (!mt.xyz.equals(tmp.xyz)) {
throw new Exception("RMS cache is inconsistent. This is the wrong tile: " + tmp);
}
mt.bytes = tmp.bytes;
if (mt.rmsBytesUsed != mt.bytes.length) {
app.debug("Size mismatch: " + mt.rmsBytesUsed + " should be " + mt.bytes.length);
mt.rmsBytesUsed = mt.bytes.length;
}
}
catch (Exception e) {
app.exception("An error occured while fetching a cached map tile from RMS.", e);
deleteFromRMS(mt);
}
}
private void keep(MapTile mt) {
// Move it to the top of the pile
cache.putLast(mt.xyz, mt);
if (mt.bytes != null && mt.rmsID == -1 && rms != null) {
// Freshly downloaded. Put it into the RMS cache.
byte[] buffer = (byte[])app.sharedBuffers.claimResourceIgnoreInterrupt();
try {
FixedByteArrayOutputStream baos = new FixedByteArrayOutputStream(buffer);
DataOutputStream dos = new DataOutputStream(baos);
mt.writeTo(dos, MapTile.WITH_BYTES);
dos.close();
baos.close();
mt.rmsID = rms.addRecord(buffer, 0, baos.count());
mt.rmsBytesUsed = mt.bytes.length;
}
catch (Exception e) {
app.exception("An error occured while caching a map tile to RMS.", e);
}
finally {
app.sharedBuffers.releaseResource(buffer);
}
}
}
synchronized void invalidate(MapTile mt) {
deleteFromRMS(mt);
mt.bytes = null;
mt.image = null;
}
private void deleteFromRMS(MapTile mt) {
if (mt.rmsID != -1 && rms != null) {
try {
app.debug("Deleting from rms: " + mt);
rms.deleteRecord(mt.rmsID);
}
catch (Exception e) {
app.exception("An error occured while deleting a cached map tile from RMS.", e);
}
finally {
mt.rmsID = -1;
mt.rmsBytesUsed = 0;
}
}
}
synchronized void cacheMaintenance() {
int memLimit = app.prefs.getInt("memCacheSize", 100) * KILO;
int rmsLimit = app.prefs.getInt("rmsCacheSize", 2000) * KILO;
int rmsReserved = app.prefs.getInt("rmsSpaceReserved", 100) * KILO;
int rmsMustFree = 0;
if (rms != null) {
try {
rmsMustFree = rmsReserved - rms.getSizeAvailable(); // if avail imageCacheSize) {
imageCount--;
freedImages++;
mt.image = null;
}
}
// Count and free memory
if (mt.bytes != null) {
memCount++;
memUsed += mt.bytes.length;
if (memUsed > memLimit) {
memCount--;
memUsed -= mt.bytes.length;
freedMem += mt.bytes.length;
mt.bytes = null; // free the memory
}
}
// Count and free RMS space
if (mt.rmsID != -1) {
rmsCount++;
rmsUsed += mt.rmsBytesUsed;
if (rmsUsed > rmsLimit || rmsMustFree > 0) {
rmsCount--;
rmsUsed -= mt.rmsBytesUsed;
rmsMustFree -= mt.rmsBytesUsed;
freedRms += mt.rmsBytesUsed;
deleteFromRMS(mt); // free the RMS space
}
}
// Note stuff that may not belong in the cache anymore
if (mt.image == null && mt.bytes == null && mt.rmsID == -1 && !mt.downloading && !mt.decoding) {
trash.addElement(mt);
}
}
// Clean up the cache itself. We keep a certain number of stale tiles around in case they are still referenced.
int trashToKeep = imageCacheSize * 10 + 100;
for (int i = trashToKeep; i < trash.size(); i++) {
MapTile mt = (MapTile)trash.elementAt(i);
synchronized (mt) {
if (mt.image == null && mt.bytes == null && mt.rmsID == -1 && !mt.downloading && !mt.decoding) {
cache.remove(mt.xyz);
}
}
}
trash.removeAllElements();
//app.debug("cacheMaintenance: used=" + imageCount + '/' + memUsed + '/' + rmsUsed + " freed=" + freedImages + '/' + freedMem + '/' + freedRms);
int callGC = app.prefs.getInt("callgc", 0);
if (callGC >= 1 && freedImages > 0) System.gc();
else if (callGC >= 2 && freedMem > 0) System.gc();
}
/** Cleans up memory that can be simply re-constructed later. */
synchronized void compact() {
for (Enumeration enum = cache.elements() ; enum.hasMoreElements() ; ) {
MapTile mt = (MapTile)enum.nextElement();
mt.image = null;
mt.bytes = null;
}
}
synchronized void openRMS() {
try {
rms = RecordStore.openRecordStore(RECORD_STORE_NAME, true);
app.debug("rms.getNextRecordID(): " + rms.getNextRecordID());
if (rms.getNextRecordID() == CACHE_INDEX_RECORD_ID) {
// Create a new empty cache index
app.debug("Creating new empty cache index.");
int id = rms.addRecord(new byte[] { CACHE_VERSION }, 0, 1);
if (id != CACHE_INDEX_RECORD_ID) {
throw new Exception("RecordStore lied! Next record ID was bogus!");
}
}
Vector tileInfo = new Vector();
byte[] buffer = (byte[])app.sharedBuffers.claimResource();
try {
// load in the cache index records
int rlen = rms.getRecord(CACHE_INDEX_RECORD_ID, buffer, 0);
ByteArrayInputStream bais = new ByteArrayInputStream(buffer, 0, rlen);
DataInputStream dis = new DataInputStream(bais);
if (dis.readByte() != CACHE_VERSION) {
app.debug("Invalid cache version");
rms.closeRecordStore();
RecordStore.deleteRecordStore(RECORD_STORE_NAME);
openRMS(); // possibly endless recursion if something goes wrong here
return;
}
while (bais.available() > 0) {
MapTile mt = new MapTile(dis, MapTile.WITH_RMSID);
tileInfo.addElement(mt);
}
dis.close();
bais.close();
}
finally {
app.sharedBuffers.releaseResource(buffer);
}
// add them to the cache
for (Enumeration enum = tileInfo.elements() ; enum.hasMoreElements() ; ) {
MapTile mt = (MapTile)enum.nextElement();
MapTile existing = (MapTile)cache.get(mt.xyz);
if (existing != null) { // probably won't happen
existing.rmsBytesUsed = mt.rmsBytesUsed;
existing.rmsID = mt.rmsID;
mt = existing;
}
cache.putLast(mt.xyz, mt);
}
}
catch (Exception e) {
app.exception("An error occured while getting the map tile cache index from RMS.", e);
}
}
synchronized void closeRMS() {
if (rms == null) return;
try {
// make sure we have plenty of space to work with
imageCacheSize = 0;
cacheMaintenance();
byte[] buffer = (byte[])app.sharedBuffers.claimResource();
try {
FixedByteArrayOutputStream fbaos = new FixedByteArrayOutputStream(buffer);
DataOutputStream dos = new DataOutputStream(fbaos);
dos.writeByte(CACHE_VERSION);
for (Enumeration enum = cache.elements() ; enum.hasMoreElements() ; ) {
MapTile mt = (MapTile)enum.nextElement();
mt.writeTo(dos, MapTile.WITH_RMSID);
}
dos.close();
fbaos.close();
rms.setRecord(CACHE_INDEX_RECORD_ID, buffer, 0, fbaos.count());
rms.closeRecordStore();
}
finally {
app.sharedBuffers.releaseResource(buffer);
}
}
catch (Exception e) {
app.exception("An error occured while closing the map tile cache RMS", e);
}
finally {
rms = null;
}
}
synchronized String validateMem() throws Exception {
if (rms == null) return "No RMS cache to validate against.";
// Check for dangling references
byte[] buffer = (byte[])app.sharedBuffers.claimResource();
try {
int missing = 0;
for (Enumeration enum = cache.elements() ; enum.hasMoreElements() ; ) {
MapTile mt = (MapTile)enum.nextElement();
if (mt.rmsID == -1) continue;
try {
if (rms.getRecord(mt.rmsID, buffer, 0) < 1) throw new Exception("Empty record");
}
catch (Exception e) {
app.debug("No such record ID: " + mt.rmsID);
missing++;
deleteFromRMS(mt);
}
}
return "Found " + missing + " dangling references.";
}
finally {
app.sharedBuffers.releaseResource(buffer);
}
}
synchronized String validateRMS() throws Exception {
if (rms == null) return "No RMS cache open.";
// Check for orphaned records
int invalid = 0;
RecordEnumeration re = rms.enumerateRecords(null, null, false);
byte[] buffer = (byte[])app.sharedBuffers.claimResource();
try {
while (re.hasNextElement()) {
int id = re.nextRecordId();
if (id == CACHE_INDEX_RECORD_ID) continue; // special record, ignore it
MapTile fromRms = null;
try {
int rlen = rms.getRecord(id, buffer, 0);
ByteArrayInputStream bais = new ByteArrayInputStream(buffer, 0, rlen);
DataInputStream din = new DataInputStream(bais);
fromRms = new MapTile(din, MapTile.WITH_MINIMUM);
fromRms.bytes = null;
fromRms.rmsID = id;
din.close();
bais.close();
MapTile mt = (MapTile)cache.get(fromRms.xyz);
if (mt == null) throw new Exception("orphaned record");
if (!mt.xyz.equals(fromRms.xyz)) throw new Exception("XYZ mismatch");
if (mt.rmsID != fromRms.rmsID) throw new Exception("ID mismatch");
}
catch (Exception e) {
if (fromRms != null) {
app.debug("Invalid record #" + id + ": " + e);
invalid++;
deleteFromRMS(fromRms); //FIXME: Should reclaim record instead of deleting it
}
}
}
}
finally {
app.sharedBuffers.releaseResource(buffer);
}
return "Found " + invalid + " invalid records.";
}
synchronized void clearCache() throws Exception {
if (rms != null) {
for (;;) {
try { rms.closeRecordStore(); }
catch (RecordStoreNotOpenException e) { break; }
}
}
RecordStore.deleteRecordStore(RECORD_STORE_NAME);
cache.clear();
openRMS();
}
synchronized String getInfo() {
String availRms;
try {
availRms = "" + (rms.getSizeAvailable() / KILO) + "KB";
}
catch (Exception e) {
availRms = e.toString();
}
return
"Map tile cache status\n\n" +
"Mem used: " + (memUsed / KILO) + "KB as " + memCount + " tiles\n" +
"Mem free: " + (Runtime.getRuntime().freeMemory() / KILO) + "KB\n" +
"Mem total: " + (Runtime.getRuntime().totalMemory() / KILO) + "KB\n\n" +
"Used RMS: " + (rmsUsed / KILO) + "KB as " + rmsCount + " tiles\n" +
"Free RMS: " + availRms + "\n";
}
}