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";
	}
}