/******************************************************************************* * Copyright (c) MOBAC developers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package mobac.gui.mapview; //License: GPL. Copyright 2008 by Jan Peter Stotz import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryNotificationInfo; import java.lang.management.MemoryPoolMXBean; import java.lang.management.MemoryUsage; import java.util.Hashtable; import javax.management.Notification; import javax.management.NotificationBroadcaster; import javax.management.NotificationListener; import mobac.gui.mapview.Tile.TileState; import mobac.program.interfaces.MapSource; import org.apache.log4j.Logger; /** * {@link TileImageCache} 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 * @author r_x */ public class MemoryTileCache implements NotificationListener { protected final Logger log; /** * Default cache size */ protected int cacheSize = 200; protected Hashtable<String, CacheEntry> hashtable; /** * List of all tiles in their last recently used order */ protected CacheLinkedListElement lruTiles; public MemoryTileCache() { log = Logger.getLogger(this.getClass()); hashtable = new Hashtable<String, CacheEntry>(cacheSize); lruTiles = new CacheLinkedListElement(); cacheSize = 500; MemoryMXBean mbean = ManagementFactory.getMemoryMXBean(); NotificationBroadcaster emitter = (NotificationBroadcaster) mbean; emitter.addNotificationListener(this, null, null); // Set-up each memory pool to notify if the free memory falls below 10% for (MemoryPoolMXBean memPool : ManagementFactory.getMemoryPoolMXBeans()) { if (memPool.isUsageThresholdSupported()) { MemoryUsage memUsage = memPool.getUsage(); memPool.setUsageThreshold((long) (memUsage.getMax() * 0.95)); } } } /** * In case we are running out of memory we free half of the cached down to a minimum of 25 cached tiles. */ public void handleNotification(Notification notification, Object handback) { log.trace("Memory notification: " + notification.toString()); if (!MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(notification.getType())) return; synchronized (lruTiles) { int count_half = lruTiles.getElementCount() / 2; count_half = Math.max(25, count_half); if (lruTiles.getElementCount() <= count_half) return; log.warn("memory low - freeing cached tiles: " + lruTiles.getElementCount() + " -> " + count_half); try { while (lruTiles.getElementCount() > count_half) { removeEntry(lruTiles.getLastElement()); } } catch (Exception e) { log.error("", e); } } } public void addTile(Tile tile) { CacheEntry entry = createCacheEntry(tile); hashtable.put(tile.getKey(), entry); lruTiles.addFirst(entry); if (hashtable.size() > cacheSize) removeOldEntries(); } public Tile getTile(MapSource source, int x, int y, int z) { CacheEntry entry = hashtable.get(Tile.getTileKey(source, x, y, z)); if (entry == null) return null; // We don't care about placeholder tiles and hourglass image tiles, the // important tiles are the loaded ones if (entry.tile.getTileState() == TileState.TS_LOADED) lruTiles.moveElementToFirstPos(entry); return entry.tile; } /** * Removes the least recently used tiles */ protected void removeOldEntries() { synchronized (lruTiles) { try { while (lruTiles.getElementCount() > cacheSize) { removeEntry(lruTiles.getLastElement()); } } catch (Exception e) { log.warn("", e); } } } protected void removeEntry(CacheEntry entry) { hashtable.remove(entry.tile.getKey()); lruTiles.removeEntry(entry); } protected CacheEntry createCacheEntry(Tile tile) { return new CacheEntry(tile); } /** * Clears the cache deleting all tiles from memory */ public void clear() { synchronized (lruTiles) { hashtable.clear(); lruTiles.clear(); } } public int getTileCount() { return hashtable.size(); } public int getCacheSize() { return cacheSize; } /** * Changes the maximum number of {@link Tile} objects that this cache holds. * * @param cacheSize * new maximum number of tiles */ public void setCacheSize(int cacheSize) { this.cacheSize = cacheSize; if (hashtable.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 { Tile tile; CacheEntry next; CacheEntry prev; protected CacheEntry(Tile tile) { this.tile = tile; } public Tile getTile() { return tile; } public CacheEntry getNext() { return next; } public CacheEntry getPrev() { return prev; } } /** * 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 = null; protected CacheEntry lastElement; protected int elementCount; public CacheLinkedListElement() { clear(); } public synchronized void clear() { elementCount = 0; firstElement = null; lastElement = null; } /** * Add the element to the head of the list. * * @param new element to be added */ public synchronized void addFirst(CacheEntry element) { 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 elemntent form the list. * * @param element * to be removed */ public synchronized void removeEntry(CacheEntry element) { 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 synchronized 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; } } }