Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
jaitools.tilecache.DiskMemTileCache Maven / Gradle / Ivy
Go to download
Provides a single jar containing all JAI-tools modules which you can
use instead of including individual modules in your project. Note:
It does not include the Jiffle scripting language or Jiffle image
operator.
/*
* Copyright 2009-2011 Michael Bedward
*
* This file is part of jai-tools.
*
* jai-tools is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* jai-tools 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with jai-tools. If not, see .
*
*/
package jaitools.tilecache;
import jaitools.CollectionFactory;
import jaitools.DaemonThreadFactory;
import java.awt.Point;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.CachedTile;
import javax.media.jai.PlanarImage;
import javax.media.jai.TileCache;
/**
* This class implements JAI {@linkplain javax.media.jai.TileCache}. It can store
* cached tiles on disk to allow applications to work with very large volumes of
* tiled image data without being limited by available memory. A subset of tiles
* (by default, the most recently accessed) are cached in memory to reduce access
* time.
*
*
* The default behaviour is to cache newly added tiles into memory. If the cache
* needs to free memory to accommodate a tile, it does so by removing lowest priority
* tiles from memory and caching them to disk. Optionally, the user can specify
* that newly added tiles are cached to disk immediately.
*
*
* Unlike the standard JAI {@code TileCache} implementation, resident tiles are cached
* using strong references. This is to support the use of this class with
* {@linkplain jaitools.tiledimage.DiskMemImage} as well as operations that need to
* cache tiles that are expensive to create (e.g. output of a time-consuming analysis).
* A disadvantage of this design is that when the cache is being used for easily
* generated tiles it can end up unnecessarily holding memory that is more urgently
* required by other parts of an application. To avoid this happening, the cache can
* be set to auto-flush resident tiles at regular intervals.
*
*
*
Implementation note
* Tile polling and auto-flushing of memory resident tiles (if enabled) both run
* on low-priority background threads. These are marked as daemon threads to
* avoid these services blocking application shutdown.
*
* @author Michael Bedward
* @author Simone Giannecchini, GeoSolutions SAS
*
* @since 1.0
* @version $Id: DiskMemTileCache.java 1525 2011-03-08 05:35:57Z michael.bedward $
*
* @see DiskCachedTile
* @see TileAccessTimeComparator
*/
public class DiskMemTileCache extends Observable implements TileCache {
/**
* The default memory capacity in bytes (64 * 2^20 = 64Mb)
* @see #setMemoryCapacity(long)
*/
public static final long DEFAULT_MEMORY_CAPACITY = 64L * 1024L * 1024L;
/**
* The default memory threshold value (0.75)
*
* @see #setMemoryThreshold(float)
*/
public static final float DEFAULT_MEMORY_THRESHOLD = 0.75F;
/**
* The default minimum period (2.5 seconds) of cache inactivity that
* must elapse before memory-resident tiles are automatically flushed.
*
* @see #setAutoFlushMemoryInterval(long)
*/
public static final long DEFAULT_AUTO_FLUSH_MEMORY_INTERVAL = 2500;
/**
* The default interval (2 seconds) for polling each tile to check if
* its owning image has been garbage collected.
*
* @see #setTilePollingInterval(long)
*/
public static final long DEFAULT_TILE_POLLING_INTERVAL = 2000L;
// @todo use JAI ParameterList or some other ready-made class for this ?
private static class ParamDesc {
String key;
Class> clazz;
Object defaultValue;
ParamDesc(String key, Class> clazz, Object defaultValue) {
this.key = key; this.clazz = clazz; this.defaultValue = defaultValue;
}
boolean typeOK (Object value) {
if (Number.class.isAssignableFrom(clazz)) {
return Number.class.isAssignableFrom(value.getClass());
} else {
return clazz.isAssignableFrom(value.getClass());
}
}
}
/**
* Key for the parameter controlling initial memory capacity of the
* tile cache. This determines the maximum number of tiles that can
* be resident concurrently. The value must be numeric and will be
* treated as Long.
* @see #setMemoryCapacity(long)
* @see #DEFAULT_MEMORY_CAPACITY
*/
public static final String KEY_INITIAL_MEMORY_CAPACITY = "memcapacity";
/**
* Key for the parameter controlling whether newly added tiles
* are immediately cached to disk as well as in memory. The value
* must be one Boolean. If the value is {@code Boolean.FALSE} (the default),
* disk caching of tiles is deferred until required (ie. when
* memory needs to be freed for other tiles).
*/
public static final String KEY_ALWAYS_DISK_CACHE = "diskcache";
/**
* Key for the parameter controlling whether the cache will auto-flush
* memory-resident tiles. The value must be Boolean. If the value is
* {@code Boolean.TRUE}, auto-flushing of resident tiles will be enabled
* when the cache is created. The default is {@code Boolean.FALSE}.
* @see #setAutoFlushMemoryEnabled(boolean)
*/
public static final String KEY_AUTO_FLUSH_MEMORY_ENABLED = "enableautoflush";
/**
* Key for the cache auto-flush interval parameter. The value must be numeric
* and represents the interval, in milliseconds, between auto-flushes of
* resident tiles. Values less than or equal to zero are ignored.
* @see #setAutoFlushMemoryInterval(long)
* @see #DEFAULT_AUTO_FLUSH_MEMORY_INTERVAL
*/
public static final String KEY_AUTO_FLUSH_MEMORY_INTERVAL = "autoflushinterval";
private static final Map paramDescriptors;
static {
ParamDesc desc;
paramDescriptors = new HashMap();
desc = new ParamDesc(KEY_INITIAL_MEMORY_CAPACITY, Number.class, DEFAULT_MEMORY_CAPACITY);
paramDescriptors.put( desc.key, desc );
desc = new ParamDesc(KEY_ALWAYS_DISK_CACHE, Boolean.class, Boolean.FALSE);
paramDescriptors.put( desc.key, desc );
desc = new ParamDesc(KEY_AUTO_FLUSH_MEMORY_ENABLED, Boolean.class, Boolean.FALSE);
paramDescriptors.put( desc.key, desc );
desc = new ParamDesc(KEY_AUTO_FLUSH_MEMORY_INTERVAL, Number.class, DEFAULT_AUTO_FLUSH_MEMORY_INTERVAL);
paramDescriptors.put( desc.key, desc );
}
// maximum memory available for resident tiles
private long memCapacity;
// current memory used for resident tiles
private long curMemory;
/*
* A value between 0.0 and 1.0 that may be used for memory control
* if the param KEY_USE_MEMORY_THRESHOLD is TRUE.
*/
private float memThreshold;
private boolean writeNewTilesToDisk;
/**
* Map of all cached tiles.
*/
protected Map tiles;
/**
* Memory-resident tiles.
*/
protected Map residentTiles;
/**
* A tile comparator used to determine the priority of tiles for
* storage in memory.
*/
private Comparator comparator;
/* List of tile references that is sorted into tile priority order when
* required for memory swapping.
*
* Implementation note: we use this in preference to a SortedSet or similar
* because of the complications of using the remove(obj) method with a sorted
* collection, where the comparator is used rather than the equals method.
*/
protected List sortedResidentTiles;
// whether to send cache diagnostics to observers
private boolean diagnosticsEnabled;
// Variables used for auto-flushing of resident tiles
private ScheduledExecutorService flushService;
private ScheduledFuture flushFuture;
private long autoFlushInterval = DEFAULT_AUTO_FLUSH_MEMORY_INTERVAL;
private AtomicBoolean okToFlush = new AtomicBoolean(false);
// Variables used for polling each tile to check if its owning image has been
// garbage collected
private final ScheduledExecutorService tilePollingService;
private ScheduledFuture tilePollingFuture;
private long tilePollingInterval = DEFAULT_TILE_POLLING_INTERVAL;
/**
* Creates a new cache with all parameters set to their default values.
*/
public DiskMemTileCache() {
this(null);
}
/**
* Creates a new cache.
*
* @param params an optional map of parameters (may be empty or {@code null})
*/
public DiskMemTileCache(Map params) {
if (params == null) {
params = Collections.emptyMap();
}
diagnosticsEnabled = false;
tiles = new HashMap();
residentTiles = CollectionFactory.map();
curMemory = 0L;
memThreshold = DEFAULT_MEMORY_THRESHOLD;
Object o;
ParamDesc desc;
desc= paramDescriptors.get(KEY_INITIAL_MEMORY_CAPACITY);
memCapacity = (Long)desc.defaultValue;
o = params.get(desc.key);
if (o != null) {
if (desc.typeOK(o)) {
memCapacity = ((Number)o).longValue();
}
}
desc = paramDescriptors.get(KEY_ALWAYS_DISK_CACHE);
writeNewTilesToDisk = (Boolean)desc.defaultValue;
o = params.get(desc.key);
if (o != null) {
if (desc.typeOK(o)) {
writeNewTilesToDisk = (Boolean)o;
}
}
desc = paramDescriptors.get(KEY_AUTO_FLUSH_MEMORY_INTERVAL);
autoFlushInterval = ((Number)desc.defaultValue).longValue();
o = params.get(desc.key);
if (o != null) {
if (desc.typeOK(o)) {
long lval = ((Number)o).longValue();
if (lval > 0) {
autoFlushInterval = lval;
}
}
}
desc = paramDescriptors.get(KEY_AUTO_FLUSH_MEMORY_ENABLED);
o = params.get(desc.key);
if (o != null) {
if (desc.typeOK(o)) {
setAutoFlushMemoryEnabled((Boolean)o);
}
}
comparator = new TileAccessTimeComparator();
sortedResidentTiles = new ArrayList();
tilePollingService = Executors.newSingleThreadScheduledExecutor(
new DaemonThreadFactory(Thread.MIN_PRIORITY, "cache-polling"));
startTilePolling();
}
/**
* Adds a tile to the cache if not already present.
*
* @param owner the image that this tile belongs to
* @param tileX the tile column
* @param tileY the tile row
* @param data the tile data
*/
public void add(RenderedImage owner, int tileX, int tileY, Raster data) {
add(owner, tileX, tileY, data, null);
}
/**
* Adds a tile to the cache if not already present.
*
* @param owner the image that this tile belongs to
* @param tileX the tile column
* @param tileY the tile row
* @param data the tile data
* @param tileCacheMetric optional tile cache metric (may be {@code null}
*/
public synchronized void add(RenderedImage owner,
int tileX,
int tileY,
Raster data,
Object tileCacheMetric) {
okToFlush.set(false);
Object key = getTileId(owner, tileX, tileY);
if (tiles.containsKey(key)) {
// tile is already cached
return;
}
try {
DiskCachedTile tile = new DiskCachedTile(
key, owner, tileX, tileY, data, writeNewTilesToDisk, tileCacheMetric);
tiles.put(key, tile);
if ( makeResident(tile, data) ) {
tile.setAction(DiskCachedTile.TileAction.ACTION_ADDED_RESIDENT);
} else {
tile.setAction(DiskCachedTile.TileAction.ACTION_ADDED);
}
if (diagnosticsEnabled) {
setChanged();
notifyObservers(tile);
}
} catch (IOException ex) {
Logger.getLogger(DiskMemTileCache.class.getName())
.log(Level.SEVERE, "Unable to cache this tile on disk", ex);
}
}
/**
* Removes a tile from the cache.
*
* @param owner the image that this tile belongs to
* @param tileX the tile column
* @param tileY the tile row
*/
public synchronized void remove(RenderedImage owner, int tileX, int tileY) {
okToFlush.set(false);
Object key = getTileId(owner, tileX, tileY);
DiskCachedTile tile = tiles.get(key);
if (tile == null) {
return;
}
if (residentTiles.containsKey(key)) {
try {
removeResidentTile(key, false);
} catch (DiskCacheFailedException ex) {
/*
* It would be nicer to just throw this exception
* upwards but we can't in the overidden method
*/
Logger.getLogger(DiskMemTileCache.class.getName()).
log(Level.SEVERE, null, ex);
}
}
tile.deleteDiskCopy();
tile.setAction(DiskCachedTile.TileAction.ACTION_REMOVED);
if (diagnosticsEnabled) {
setChanged();
notifyObservers(tile);
}
tiles.remove(key);
}
/**
* Gets the specified tile from the cache if present. If the tile is
* cached but not resident in memory it will be read from the cache's
* disk storage and made resident.
*
* @param owner the image that the tile belongs to
* @param tileX the tile column
* @param tileY the tile row
* @return the requested tile or {@code null} if the tile was not cached
*/
public synchronized Raster getTile(RenderedImage owner, int tileX, int tileY) {
okToFlush.set(false);
Raster r = null;
Object key = getTileId(owner, tileX, tileY);
DiskCachedTile tile = tiles.get(key);
if (tile != null) {
// is the tile resident ?
r = residentTiles.get(key);
if (r == null) {
/*
* The tile is not resident. Attempt
* to read it from the disk.
*/
r = tile.readData();
if (r == null) {
/* The tile was not cached on disk. It may have
* been resident only, and then flushed.
*/
return null;
}
if (makeResident(tile, r)) {
tile.setAction(DiskCachedTile.TileAction.ACTION_RESIDENT);
if (diagnosticsEnabled) {
setChanged();
notifyObservers(tile);
}
}
}
tile.setAction(DiskCachedTile.TileAction.ACTION_ACCESSED);
tile.setTileTimeStamp(System.currentTimeMillis());
if (diagnosticsEnabled) {
setChanged();
notifyObservers(tile);
}
}
return r;
}
/**
* Gets all cached tiles associated with the given image.
* The tiles will be loaded into memory as space allows.
*
* @param owner the image for which tiles are requested
* @return an array of tile Rasters
*/
public synchronized Raster[] getTiles(RenderedImage owner) {
okToFlush.set(false);
int minX = owner.getMinTileX();
int minY = owner.getMinTileY();
int numX = owner.getNumXTiles();
int numY = owner.getNumYTiles();
List keys = new ArrayList();
for (int y = minY, ny=0; ny < numY; y++, ny++) {
for (int x = minX, nx = 0; nx < numX; x++, nx++) {
Object key = getTileId(owner, x, y);
if (tiles.containsKey(key)) keys.add(key);
}
}
Raster[] rasters = new Raster[keys.size()];
int k = 0;
for (Object key : keys) {
DiskCachedTile tile = tiles.get(key);
Raster r = residentTiles.get(tile.getTileId());
if (r == null) {
r = tile.readData();
makeResident(tile, r);
}
rasters[k++] = r;
tile.setTileTimeStamp(System.currentTimeMillis());
tile.setAction(DiskCachedTile.TileAction.ACTION_ACCESSED);
if (diagnosticsEnabled) {
setChanged();
notifyObservers(tile);
}
}
return rasters;
}
/**
* Removes all tiles that belong to the given image from the cache.
*
* @param owner the image owning the tiles to be removed
*/
public void removeTiles(RenderedImage owner) {
for (int y = owner.getMinTileY(), ny=0; ny < owner.getNumYTiles(); y++, ny++) {
for (int x = owner.getMinTileX(), nx=0; nx < owner.getNumXTiles(); x++, nx++) {
remove(owner, x, y);
}
}
}
/**
* Sets the interval between polling each tile to check if its owning image
* has been garbage collected. Any such tiles are removed from the
* cache.
*
* @param interval interval in milliseconds
* (values less than or equal to zero are ignored)
*/
public void setTilePollingInterval(long interval) {
if (interval > 0 && interval != tilePollingInterval) {
stopTilePolling();
tilePollingInterval = interval;
startTilePolling();
}
}
/**
* Sets the interval between polling each tile to check if its owning image
* has been garbage collected. Any such tiles are removed from the
* cache. *
* @return interval in milliseconds
*/
public long getTilePollingInterval() {
return tilePollingInterval;
}
/**
* Starts the tile polling task which calls {@link #removeNullTiles()}
* at a fixed interval.
*/
private void startTilePolling() {
if (!isPollingTiles()) {
tilePollingFuture = tilePollingService.scheduleAtFixedRate(
new Runnable() {
public void run() {
removeNullTiles();
}
},
tilePollingInterval,
tilePollingInterval,
TimeUnit.MILLISECONDS);
}
}
/**
* Stops the tile polling task.
*/
private void stopTilePolling() {
if (isPollingTiles()) {
tilePollingFuture.cancel(true);
tilePollingFuture = null;
}
}
private boolean isPollingTiles() {
return tilePollingFuture != null && !tilePollingFuture.isDone();
}
/**
* Checks if any tiles have a {@code null} owner (e.g. owning image has been
* garbage collected) and, if so, removes them from the cache.
*/
private void removeNullTiles() {
Set nullTileKeys = CollectionFactory.set();
for (Object key : tiles.keySet()) {
DiskCachedTile tile = tiles.get(key);
if (tile.getOwner() == null) {
nullTileKeys.add(key);
}
}
for (Object key : nullTileKeys) {
DiskCachedTile tile = tiles.get(key);
tile.deleteDiskCopy();
if (residentTiles.containsKey(key)) {
residentTiles.remove(key);
sortedResidentTiles.remove(tile);
curMemory -= tile.getTileSize();
}
tiles.remove(key);
}
}
/**
* Adds all tiles for the given image to the cache.
*
* @param owner the image that the tiles belong to
* @param tileIndices an array of Points specifying the column-row coordinates
* of each tile
* @param tiles tile data in the form of Raster objects
* @param tileCacheMetric optional metric (may be {@code null})
*/
public synchronized void addTiles(RenderedImage owner,
Point[] tileIndices,
Raster[] tiles,
Object tileCacheMetric) {
if (tileIndices.length != tiles.length) {
throw new IllegalArgumentException("tileIndices and tiles args must be the same length");
}
for (int i = 0; i < tiles.length; i++) {
add(owner, tileIndices[i].x, tileIndices[i].y, tiles[i], tileCacheMetric);
}
}
/**
* Gets the specified tiles for the given image.
*
* @param owner the image that the tiles belong to
* @param tileIndices an array of Points specifying the column-row coordinates
* of each tile
* @return data for the requested tiles as Raster objects
*/
public synchronized Raster[] getTiles(RenderedImage owner, Point[] tileIndices) {
Raster[] r = null;
if (tileIndices.length > 0) {
r = new Raster[tileIndices.length];
for (int i = 0; i < tileIndices.length; i++) {
r[i] = getTile(owner, tileIndices[i].x, tileIndices[i].y);
}
}
return r;
}
/**
* Removes ALL tiles from the cache: all resident tiles will be
* removed from memory and all files for disk-cached tiles will
* be discarded.
*
* The update action of each tile will be set to {@linkplain DiskCachedTile#ACTION_REMOVED}.
*/
public synchronized void flush() {
flushMemory();
for (DiskCachedTile tile : tiles.values()) {
tile.deleteDiskCopy();
tile.setAction(DiskCachedTile.TileAction.ACTION_REMOVED);
if (diagnosticsEnabled) {
setChanged();
notifyObservers(tile);
}
}
tiles.clear();
}
/**
* Removes all resident tiles from memory. No rewriting of tile data
* to disk is done.
*/
public synchronized void flushMemory() {
residentTiles.clear();
sortedResidentTiles.clear();
curMemory = 0;
}
/**
* Frees memory for resident tiles so that the fraction of memory occupied is
* no more than the current value of the mamory threshold.
*
* @see DiskMemTileCache#setMemoryThreshold(float)
*/
public synchronized void memoryControl() {
long maxUsed = (long)(memThreshold * memCapacity);
long toFree = curMemory - maxUsed;
if (toFree > 0) {
defaultMemoryControl( toFree );
}
}
/**
* Makes the requested amount of memory cache available, removing
* resident tiles as necessary.
*
* @param memRequired memory requested (bytes)
*/
private void defaultMemoryControl( long memRequired ) {
if (memRequired > memCapacity) {
// @todo something better than this...
throw new RuntimeException("space required is greater than cache memory capacity");
}
/*
* Remove one or more lowest priority tiles to free
* space
*/
Collections.sort(sortedResidentTiles, comparator);
while (memCapacity - curMemory < memRequired && !sortedResidentTiles.isEmpty()) {
Object key = sortedResidentTiles.get(sortedResidentTiles.size()-1).getTileId();
try {
removeResidentTile(key, true);
} catch (DiskCacheFailedException ex) {
/*
* It would be nicer to just throw this exception
* upwards be we can't in the overidden method
*/
Logger.getLogger(DiskMemTileCache.class.getName()).
log(Level.SEVERE, null, ex);
}
}
}
/**
* Does nothing.
*
* @param arg0
* @deprecated
*/
@Deprecated
public void setTileCapacity(int arg0) {
}
/**
* Always returns 0.
*
* @deprecated
*/
@Deprecated
public int getTileCapacity() {
return 0;
}
/**
* Resets the memory capacity of the cache. Setting capacity to 0 will
* flush all resident tiles from memory. Setting a capacity less than the
* current capacity could cause some memory-resident tiles being
* removed from memory.
*
* @param newCapacity requested memory capacity for resident tiles
*/
public synchronized void setMemoryCapacity(long newCapacity) {
if (newCapacity < 0) {
throw new IllegalArgumentException("memory capacity must be >= 0");
}
long oldCapacity = memCapacity;
memCapacity = newCapacity;
if (newCapacity == 0) {
flushMemory();
} else if (newCapacity < oldCapacity && curMemory > newCapacity) {
/*
* Note: we free memory here directly rather than using
* memoryControl or defaultMemoryControl methods because
* they will fail when memCapacity has been reduced
*/
Collections.sort(sortedResidentTiles, comparator);
while (curMemory > newCapacity) {
Object key = sortedResidentTiles.get(sortedResidentTiles.size()-1).getTileId();
try {
removeResidentTile(key, true);
} catch (DiskCacheFailedException ex) {
/*
* It would be nicer to just throw this exception
* upwards be we can't in the overidden method
*/
Logger.getLogger(DiskMemTileCache.class.getName()).
log(Level.SEVERE, null, ex);
}
}
}
}
/**
* Gets the amount of memory, in bytes, allocated for storage of
* resident tiles.
*
* @return resident tile memory capacity in bytes
*/
public long getMemoryCapacity() {
return memCapacity;
}
/**
* Gets the amount of memory currently being used for storage of
* memory-resident tiles.
*
* @return current memory use in bytes
*/
public long getCurrentMemory() {
return curMemory;
}
/**
* Sets the memoryThreshold value to a floating point number that ranges from
* 0.0 to 1.0. When the cache memory is full, the memory usage will be reduced
* to this fraction of the total cache memory capacity. For example, a value
* of .75 will cause 25% of the memory to be cleared, while retaining 75%.
*
* @param memoryThreshold Retained fraction of memory
* @throws IllegalArgumentException if the memoryThreshold is less than 0.0 or greater than 1.0
*/
public void setMemoryThreshold(float newThreshold) {
if (newThreshold < 0.0F) {
memThreshold = 0.0F;
} else if (newThreshold > 1.0F) {
memThreshold = 1.0F;
} else {
memThreshold = newThreshold;
}
memoryControl();
}
/**
* Returns the memory threshold, which is the fractional amount of cache memory
* to retain during tile removal. This only applies if memory thresholding has
* been enabled by passing the parameter {@linkplain #KEY_USE_MEMORY_THRESHOLD} to
* the constructor with a value of {@code Boolean.TRUE}.
*
* @return the retained fraction of memory
*/
public float getMemoryThreshold() {
return memThreshold;
}
/**
* Sets the comparator to use to assign memory-residence priority to
* tiles. If {@code comp} is {@code null} the default comparator
* ({@link TileAccessTimeComparator}) will be used.
*
* @param comp the comparator or {@code null} for the default
*/
public synchronized void setTileComparator(Comparator comp) {
if ( comp == null ) {
// switch to default comparator based on tile access time
comparator = new TileAccessTimeComparator();
} else {
comparator = comp;
}
sortedResidentTiles = new ArrayList();
for (Object key : residentTiles.keySet()) {
sortedResidentTiles.addAll(tiles.values());
}
Collections.sort(sortedResidentTiles, comparator);
}
/**
* Gets the comparator currently used to assign memory-residence
* priority to tiles.
*
* @return the current comparator
*/
public Comparator getTileComparator() {
return comparator;
}
/**
* Gets the total number of tiles currently in the cache.
*
* @return number of cached tiles
*/
public int getNumTiles() {
return tiles.size();
}
/**
* Gets the number of tiles currently residing in the
* cache's memory storage.
*
* @return number of memory-resident tiles
*/
public int getNumResidentTiles() {
return residentTiles.size();
}
/**
* Checks whether a given tile is in this cache.
*
* @param owner the owning image
* @param tileX tile column
* @param tileY tile row
* @return {@code true} if the cache contains the tile; {@code false} otherwise
*/
public boolean containsTile(RenderedImage owner, int tileX, int tileY) {
Object key = getTileId(owner, tileX, tileY);
return tiles.containsKey(key);
}
/**
* Checks whether a given tile is in this cache's memory storage.
*
* @param owner the owning image
* @param tileX tile column
* @param tileY tile row
* @return {@code true} if the tile is in cache memory; {@code false} otherwise
*/
public boolean containsResidentTile(RenderedImage owner, int tileX, int tileY) {
Object key = getTileId(owner, tileX, tileY);
return residentTiles.containsKey(key);
}
/**
* Informs the cache that a tile's data have changed. The tile should
* be resident in memory as the result of a previous {@code getTile}
* request. If this is the case and the tile was previously written to
* disk, then the cache's disk copy of the tile will be refreshed.
*
* If the tile is not resident in memory, for instance
* because of memory swapping for other tile accesses, the disk copy
* will not be refreshed and a {@code TileNotResidentException} is
* thrown.
*
* @param owner the owning image
* @param tileX tile column
* @param tileY tile row
* @throws TileNotResidentException if the tile is not resident
*/
public void setTileChanged(RenderedImage owner, int tileX, int tileY)
throws TileNotResidentException, DiskCacheFailedException {
okToFlush.set(false);
Object tileId = getTileId(owner, tileX, tileY);
Raster r = residentTiles.get(tileId);
if (r == null) {
throw new TileNotResidentException(owner, tileX, tileY);
}
DiskCachedTile tile = tiles.get(tileId);
if (tile.cachedToDisk()) {
try {
tile.writeData(r);
} catch (IOException ioEx) {
throw new DiskCacheFailedException(owner, tileX, tileY);
}
}
}
/**
* Enables or disables auto-flushing of memory resident with the
* currently set minimum interval.
*
* @param enable {@code true} to enable auto-flushing; {@code false} to disable
* @see #setAutoFlushMemoryInterval(long)
*/
public final void setAutoFlushMemoryEnabled(boolean enable) {
if (enable) {
if (!isAutoFlushMemoryEnabled()) {
if (flushService == null) {
flushService = Executors.newSingleThreadScheduledExecutor(
new DaemonThreadFactory(Thread.MIN_PRIORITY, "cache-flush"));
}
flushFuture = flushService.scheduleWithFixedDelay(
new Runnable() {
public void run() {
if (okToFlush.getAndSet(true)) {
flushMemory();
}
}
},
autoFlushInterval,
autoFlushInterval,
TimeUnit.MILLISECONDS);
}
} else if (isAutoFlushMemoryEnabled()) {
flushFuture.cancel(true);
}
}
/**
* Checks whether auto-flushing of memory-resident tiles is currently enabled.
*
* @return {@code true} if the cache is auto-flushing; {@code false} otherwise
*/
public boolean isAutoFlushMemoryEnabled() {
return (flushFuture != null && !flushFuture.isDone());
}
/**
* Sets the minimum period of cache inactivity, in milliseconds, that must
* elapse before automatically flushing memory-resident tiles.
*
* @param interval interval in milliseconds
* (values less than or equal to zero are ignored)
*/
public void setAutoFlushMemoryInterval(long interval) {
if (interval > 0 && interval != autoFlushInterval) {
if (isAutoFlushMemoryEnabled()) {
setAutoFlushMemoryEnabled(false);
}
autoFlushInterval = interval;
setAutoFlushMemoryEnabled(true);
}
}
/**
* Gets the current auto-flush interval. This is the minimum period of
* cache inactivity, in milliseconds, that must elapse before
* automatically flushing tiles.
*
* @return interval in milliseconds
*/
public long getAutoFlushMemoryInterval() {
return autoFlushInterval;
}
/**
* Enables or disables the publishing of cache messages to Observers.
*
* @param state {@code true} to publish diagnostic messages; {@code false} to suppress them
*/
public void setDiagnostics(boolean state) {
diagnosticsEnabled = state;
}
/**
* Accepts a {@code DiskMemCacheVisitor} object and calls its
* {@code visit} method for each tile in the cache.
*/
public synchronized void accept(DiskMemTileCacheVisitor visitor) {
for (Object key : tiles.keySet()) {
visitor.visit(tiles.get(key), residentTiles.containsKey(key));
}
}
/**
* Adds a raster to those resident in memory.
*/
private boolean makeResident(DiskCachedTile tile, Raster data) {
okToFlush.set(false);
if (tile.getTileSize() > memCapacity) {
return false;
}
if (tile.getTileSize() > memCapacity - curMemory) {
memoryControl();
/*
* It is possible that the threshold rule fails to
* free enough memory for the tile
*/
if (tile.getTileSize() > memCapacity - curMemory) {
defaultMemoryControl(tile.getTileSize());
}
}
residentTiles.put(tile.getTileId(), data);
curMemory += tile.getTileSize();
/*
* We don't bother about sort order here. Instead, the list
* will be sorted by tile priority when resident tiles are
* being removed
*/
sortedResidentTiles.add(tile);
return true;
}
/**
* Removes a tile from the cache's memory storage. This may be to free
* space for other tiles, in which case {@code writeData} will be
* set to {@code true} and, if the tile is writable, a request is made to write
* its data to disk again. If the tile is being removed from the cache
* entirely, this method will be called with {@code writeData} set
* to {@code false}.
*
* @param tileId the tile's unique id
* @param writeData if {@code true}, and the tile is writable, its data will be
* written to disk again; otherwise no writing is done.
*/
private void removeResidentTile(Object tileId, boolean writeData) throws DiskCacheFailedException {
okToFlush.set(false);
DiskCachedTile tile = tiles.get(tileId);
Raster raster = residentTiles.remove(tileId);
sortedResidentTiles.remove(tile);
curMemory -= tile.getTileSize();
/**
* If the tile is writable, ie. its data are represented
* by a WritableRaster, we cache it to disk
*/
if (writeData && tile.isWritable()) {
try {
tile.writeData(raster);
} catch (IOException ioEx) {
throw new DiskCacheFailedException(tile.getOwner(), tile.getTileX(), tile.getTileY());
}
}
tile.setAction(DiskCachedTile.TileAction.ACTION_NON_RESIDENT);
if (diagnosticsEnabled) {
setChanged();
notifyObservers(tile);
}
}
/**
* Generates a unique ID for this tile. This uses the same technique as the
* Sun memory cache implementation: putting the id of the owning image
* into the upper bytes of a long or BigInteger value and the tile index into
* the lower bytes.
* @param owner the owning image
* @param tileX tile column
* @param tileY tile row
* @return the ID as an Object which will be an instance of either Long or BigInteger
*/
private Object getTileId(RenderedImage owner,
int tileX,
int tileY) {
long tileId = tileY * (long)owner.getNumXTiles() + tileX;
BigInteger imageId = null;
if (owner instanceof PlanarImage) {
imageId = (BigInteger)((PlanarImage)owner).getImageID();
}
if (imageId != null) {
byte[] buf = imageId.toByteArray();
int length = buf.length;
byte[] buf1 = new byte[buf.length + 8];
System.arraycopy(buf, 0, buf1, 0, length);
for (int i = 7, j = 0; i >= 0; i--, j += 8)
buf1[length++] = (byte)(tileId >> j);
return new BigInteger(buf1);
} else {
tileId &= 0x00000000ffffffffL;
return new Long(((long)owner.hashCode() << 32) | tileId);
}
}
}