// Created by plusminus on 22:13:10 - 28.09.2008
package de.blau.android.views.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import com.drew.lang.annotations.NotNull;
import android.graphics.Bitmap;
import android.util.Log;
import de.blau.android.exception.StorageException;
/**
* Simple LRU cache for any type of object. Implemented as an extended
* <code>HashMap</code> with a maximum size and an aggregated <code>List</code>
* as LRU queue.
* <br/>
* This class was taken from OpenStreetMapViewer (original package org.andnav.osm) in 2010-06
* by Marcus Wolschon to be integrated into the de.blau.androin
* OSMEditor.
* @author Nicolas Gramlich
* @author Marcus Wolschon <Marcus@Wolschon.biz>
*
*/
public class LRUMapTileCache {
private static final String DEBUG_TAG = "LRUMapTileCache";
// ===========================================================
// Constants
// ===========================================================
// ===========================================================
// Fields
// ===========================================================
HashMap<String,CacheElement> cache;
/** Maximum cache size. */
private long maxCacheSize;
/** Current cache size **/
private long cacheSize = 0;
/** LRU list. */
private final LinkedList<CacheElement> list;
private final ArrayList<CacheElement> reuseList;
private class CacheElement {
boolean recycleable = true;
String key;
Bitmap bitmap;
long owner;
public CacheElement(@NotNull String key, @NotNull Bitmap bitmap, boolean recycleable, long owner) {
init(key, bitmap, recycleable, owner);
}
void init(@NotNull String key, @NotNull Bitmap bitmap, boolean recycleable, long owner) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
if (bitmap == null) {
throw new IllegalArgumentException("bitmap cannot be null");
}
this.recycleable = recycleable;
this.key = key;
this.bitmap = bitmap;
this.owner = owner;
}
}
// ===========================================================
// Constructors
// ===========================================================
/**
* Constructs a new LRU cache instance.
*
* @param maxCacheSize the maximum number of entries in this cache before entries are aged off.
*/
public LRUMapTileCache(final long maxCacheSize) {
super();
// Log.d("LRUMapTileCache","created");
this.maxCacheSize = maxCacheSize;
cache = new HashMap<String,CacheElement>();
list = new LinkedList<CacheElement>();
reuseList = new ArrayList<CacheElement>();
}
// ===========================================================
// Getter & Setter
// ===========================================================
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
/**
* Overrides clear() to also clear the LRU list.
*/
public synchronized void clear() {
for (CacheElement ce:cache.values()) {
Bitmap b = ce.bitmap;
if (b != null && ce.recycleable) {
b.recycle();
}
}
cache.clear();
list.clear();
}
/**
* Ensure the cache is less than its limit, less some extra.
* @param extra Extra space to take away from the cache size. Used to make room
* for new items before adding them so that the total cache never exceeds the limit.
*/
private synchronized boolean applyCacheLimit(long extra, long owner) {
long limit = maxCacheSize - extra;
if (limit < 0) {
limit = 0;
}
while (cacheSize > limit && !list.isEmpty()) {
// Log.d(DEBUG_TAG,"removing bitmap from in memory cache " + cacheSize);
CacheElement ce = list.removeLast();
if (ce.owner == owner && owner != 0) {
// cache is being thrashed because it is too small, fail
Log.d(DEBUG_TAG,"cache too small, failing");
return false;
}
if (cache.remove(ce.key) == null) {
throw new IllegalStateException("can't remove " + ce.key + " from cache");
}
reuseList.add(ce);
Bitmap b = ce.bitmap;
if (b != null && !b.isRecycled()) {
cacheSize -= b.getRowBytes() * b.getHeight();
if (ce.recycleable) {
b.recycle();
}
}
}
return true; // success
}
/**
* Current number of entries
* @return count
*/
public int size() {
return cache.size();
}
/**
* Reduces memory use by halving the cache size.
*/
public void onLowMemory() {
maxCacheSize /= 2;
applyCacheLimit(0, 0);
}
public synchronized boolean containsKey(String key) {
return cache.containsKey(key);
}
/**
* Calculate the amount of memory used by the cache.
* @return The number of bytes used by the cache.
*/
public long cacheSizeBytes() {
return cacheSize;
}
public long getMaxCacheSize() {
return maxCacheSize;
}
/**
* Overrides <code>put()</code> so that it also updates the LRU list. Interesting enough the slight change in signature does work
*
* @param key
* key with which the specified value is to be associated
* @param value
* value to be associated with the key
* @return previous value associated with key or <code>null</code> if there
* was no mapping for key; a <code>null</code> return can also
* indicate that the cache previously associated <code>null</code>
* with the specified key
* @throws StorageException
*/
public synchronized Bitmap put(final String key, final Bitmap value, boolean recycleable, long owner) throws StorageException {
// Log.d("LRUMapTileCache","put " + key + " " + recycleable);
if (maxCacheSize == 0 || value == null){
return null;
}
// if the key isn't in the cache and the cache is full...
if (!containsKey(key)) {
long bitmapSize = value.getRowBytes() * value.getHeight();
if (!applyCacheLimit(bitmapSize*2, owner)) {
// failed: cache is to small to handle all tiles necessary for one draw cycle
// see if we can expand by 50%
if (maxCacheSize < (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory()) && (maxCacheSize/2 > bitmapSize)) {
Log.w(DEBUG_TAG,"expanding memory tile cache from " + maxCacheSize + " to " + (maxCacheSize + maxCacheSize/2));
maxCacheSize = maxCacheSize + maxCacheSize/2;
} else {
throw new StorageException(StorageException.OOM); // can't expand any more
}
}
// avoid creating new objects
CacheElement ce = null;
if (!reuseList.isEmpty()) {
ce = reuseList.remove(0);
ce.init(key, value, recycleable, owner);
} else {
ce = new CacheElement(key, value, recycleable, owner);
}
list.addFirst(ce);
cache.put(key, ce);
cacheSize += bitmapSize;
} else {
update(key);
}
// Log.d("LRUMapTileCache","put done");
return value;
}
/**
* Overrides <code>get()</code> so that it also updates the LRU list.
*
* @param key
* key with which the expected value is associated
* @return the value to which the cache maps the specified key, or
* <code>null</code> if the map contains no mapping for this key
*/
public synchronized Bitmap get(final String key) {
final CacheElement value = cache.get(key);
// Log.d("LRUMapTileCache","get " + key);
if (value != null) {
update(value);
return value.bitmap;
}
// Log.d("LRUMapTileCache","get done");
return null;
}
/**
* Moves the specified value to the top of the LRU list (the bottom of the
* list is where least recently used items live).
*
* @param key of the value to move to the top of the list
*/
private synchronized void update(final String key) {
CacheElement ce = cache.get(key);
if (ce != null) {
update(ce);
}
}
/**
* Moves the specified value to the top of the LRU list (the bottom of the
* list is where least recently used items live).
*
* @param value to move to the top of the list
*/
private synchronized void update(final CacheElement value) {
list.remove(value);
list.addFirst(value);
}
// ===========================================================
// Methods
// ===========================================================
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
}