// License: GPL. For details, see Readme.txt file.
package org.openstreetmap.gui.jmapviewer;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
/**
* {@link TileCache} implementation that stores all {@link Tile} objects in
* memory up to a certain limit ({@link #getCacheSize()}). If the limit is
* exceeded the least recently used {@link Tile} objects will be deleted.
*
* @author Jan Peter Stotz
*/
public class MemoryTileCache implements TileCache {
protected static final Logger log = Logger.getLogger(MemoryTileCache.class.getName());
/**
* Default cache size
*/
protected int cacheSize;
protected final Map<String, CacheEntry> hash;
/**
* List of all tiles in their last recently used order
*/
protected final CacheLinkedListElement lruTiles;
/**
* Constructs a new {@code MemoryTileCache}.
*/
public MemoryTileCache() {
this(200);
}
/**
* Constructs a new {@code MemoryTileCache}.
* @param cacheSize size of the cache
*/
public MemoryTileCache(int cacheSize) {
this.cacheSize = cacheSize;
hash = new HashMap<>(cacheSize);
lruTiles = new CacheLinkedListElement();
}
@Override
public synchronized void addTile(Tile tile) {
CacheEntry entry = createCacheEntry(tile);
if (hash.put(tile.getKey(), entry) == null) {
// only if hash hadn't had the element, add it to LRU
lruTiles.addFirst(entry);
if (hash.size() > cacheSize || lruTiles.getElementCount() > cacheSize) {
removeOldEntries();
}
}
}
@Override
public synchronized Tile getTile(TileSource source, int x, int y, int z) {
CacheEntry entry = hash.get(Tile.getTileKey(source, x, y, z));
if (entry == null)
return null;
lruTiles.moveElementToFirstPos(entry);
return entry.tile;
}
/**
* Removes the least recently used tiles
*/
protected synchronized void removeOldEntries() {
try {
while (lruTiles.getElementCount() > cacheSize) {
removeEntry(lruTiles.getLastElement());
}
} catch (NullPointerException e) {
log.warning(e.getMessage());
}
}
protected synchronized void removeEntry(CacheEntry entry) {
hash.remove(entry.tile.getKey());
lruTiles.removeEntry(entry);
}
protected CacheEntry createCacheEntry(Tile tile) {
return new CacheEntry(tile);
}
@Override
public synchronized void clear() {
hash.clear();
lruTiles.clear();
}
@Override
public synchronized int getTileCount() {
return hash.size();
}
@Override
public synchronized int getCacheSize() {
return cacheSize;
}
/**
* Changes the maximum number of {@link Tile} objects that this cache holds.
*
* @param cacheSize
* new maximum number of tiles
*/
public synchronized void setCacheSize(int cacheSize) {
this.cacheSize = cacheSize;
if (hash.size() > cacheSize)
removeOldEntries();
}
/**
* Linked list element holding the {@link Tile} and links to the
* {@link #next} and {@link #prev} item in the list.
*/
protected static class CacheEntry {
private Tile tile;
private CacheEntry next;
private CacheEntry prev;
protected CacheEntry(Tile tile) {
this.tile = tile;
}
@Override
public String toString() {
return tile.toString();
}
}
/**
* Special implementation of a double linked list for {@link CacheEntry}
* elements. It supports element removal in constant time - in difference to
* the Java implementation which needs O(n).
*
* @author Jan Peter Stotz
*/
protected static class CacheLinkedListElement {
protected CacheEntry firstElement;
protected CacheEntry lastElement;
protected int elementCount;
/**
* Constructs a new {@code CacheLinkedListElement}.
*/
public CacheLinkedListElement() {
clear();
}
public void clear() {
elementCount = 0;
firstElement = null;
lastElement = null;
}
/**
* Add the element to the head of the list.
*
* @param element new element to be added
*/
public void addFirst(CacheEntry element) {
if (element == null) return;
if (elementCount == 0) {
firstElement = element;
lastElement = element;
element.prev = null;
element.next = null;
} else {
element.next = firstElement;
firstElement.prev = element;
element.prev = null;
firstElement = element;
}
elementCount++;
}
/**
* Removes the specified element from the list.
*
* @param element element to be removed
*/
public void removeEntry(CacheEntry element) {
if (element == null) return;
if (element.next != null) {
element.next.prev = element.prev;
}
if (element.prev != null) {
element.prev.next = element.next;
}
if (element == firstElement)
firstElement = element.next;
if (element == lastElement)
lastElement = element.prev;
element.next = null;
element.prev = null;
elementCount--;
}
public void moveElementToFirstPos(CacheEntry entry) {
if (firstElement == entry)
return;
removeEntry(entry);
addFirst(entry);
}
public int getElementCount() {
return elementCount;
}
public CacheEntry getLastElement() {
return lastElement;
}
public CacheEntry getFirstElement() {
return firstElement;
}
}
}