/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* 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 3 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 com.bc.ceres.jai.tilecache;
import com.sun.media.jai.util.CacheDiagnostics;
import com.sun.media.jai.util.ImageUtil;
import javax.media.jai.EnumeratedParameter;
import javax.media.jai.TileCache;
import javax.media.jai.JAI;
import javax.media.jai.util.ImagingListener;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Observable;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Logger;
/**
* A tile cache based on Sun Microsystems' reference implementation of the
* <code>javax.media.jai.TileCache</code> interface. In opposite to the
* Sun implementation, we'll never throw away any tiles but instead swap them to a
* {@link SwapSpace}.
*
* @author Sun Microsystems
* @author Norman Fomferra
*/
public final class SwappingTileCache extends Observable implements TileCache, CacheDiagnostics {
/**
* The default memory capacity of the cache (16 MB).
*/
public static final long DEFAULT_MEMORY_CAPACITY = 16L * 1024L * 1024L;
/**
* The default hashtable capacity (heuristic)
*/
public static final int DEFAULT_HASHTABLE_CAPACITY = 1009; // prime number
/**
* The default directory where tiles are stored if they don't fit into memory anymore.
*/
public static final File DEFAULT_SWAP_DIR = new File(System.getProperty("java.io.tmpdir"));
/**
* The hashtable load factor
*/
private static final float LOAD_FACTOR = 0.5F;
/**
* The tile cache.
* A Hashtable is used to cache the tiles. The "key" is a
* <code>Object</code> determined based on tile owner's UID if any or
* hashCode if the UID doesn't exist, and tile index. The
* "value" is a SunCachedTile.
*/
private Hashtable<Object, MemoryTile> cache;
/**
* Sorted (Tree) Set used with tile metrics.
* Adds another level of metrics used to determine
* which tiles are removed during memoryControl().
*/
private SortedSet<MemoryTile> cacheSortedSet;
/**
* The memory capacity of the cache.
*/
private long memoryCapacity;
/**
* The amount of memory currently being used by the cache.
*/
private long memoryUsage = 0;
/**
* The amount of memory to keep after memory control
*/
private float memoryThreshold = 0.75F;
/**
* A indicator for tile access time.
*/
private long timeStamp = 0;
/**
* Custom comparator used to determine tile cost or
* priority ordering in the tile cache.
*/
private Comparator comparator = null;
/**
* Pointer to the first (newest) tile of the linked SunCachedTile list.
*/
private MemoryTile first = null;
/**
* Pointer to the last (oldest) tile of the linked SunCachedTile list.
*/
private MemoryTile last = null;
/**
* Tile count used for diagnostics
*/
private long tileCount = 0;
/**
* Cache hit count
*/
private long hitCount = 0;
/**
* Cache miss count
*/
private long missCount = 0;
/**
* Diagnostics enable/disable
*/
private boolean diagnostics = false;
private SwapSpace swapSpace;
// diagnostic actions
// !!! If actions are changed in any way (removal, modification, addition)
// then the getCachedTileActions() method below should be changed to match.
private static final int ADD = 0;
private static final int REMOVE = 1;
private static final int REMOVE_FROM_FLUSH = 2;
private static final int REMOVE_FROM_MEMCON = 3;
private static final int UPDATE_FROM_ADD = 4;
private static final int UPDATE_FROM_GETTILE = 5;
private static final int ABOUT_TO_REMOVE = 6;
/**
* @return An array of <code>EnumeratedParameter</code>s corresponding
* to the numeric values returned by the <code>getAction()</code>
* method of the <code>CachedTile</code> implementation used by
* <code>SunTileCache</code>. The "name" of each
* <code>EnumeratedParameter</code> provides a brief string
* describing the numeric action value.
*/
public static EnumeratedParameter[] getCachedTileActions() {
return new EnumeratedParameter[]{
new EnumeratedParameter("add", ADD),
new EnumeratedParameter("remove", REMOVE),
new EnumeratedParameter("remove_by_flush", REMOVE_FROM_FLUSH),
new EnumeratedParameter("remove_by_memorycontrol",
REMOVE_FROM_MEMCON),
new EnumeratedParameter("timestamp_update_by_add", UPDATE_FROM_ADD),
new EnumeratedParameter("timestamp_update_by_gettile",
UPDATE_FROM_GETTILE),
new EnumeratedParameter("preremove", ABOUT_TO_REMOVE)
};
}
/**
* No args constructor. Use the DEFAULT_MEMORY_CAPACITY of 16 Megs.
*/
public SwappingTileCache() {
this(DEFAULT_MEMORY_CAPACITY, new DefaultSwapSpace(DEFAULT_SWAP_DIR));
}
/**
* Constructor. The memory capacity should be explicitly specified.
*
* @param memoryCapacity The maximum cache memory size in bytes.
* @param swapSpace The space used to swap out tiles.
* @throws IllegalArgumentException If <code>memoryCapacity</code>
* is less than 0.
*/
public SwappingTileCache(long memoryCapacity, SwapSpace swapSpace) {
if (memoryCapacity < 0) {
throw new IllegalArgumentException("memoryCapacity < 0");
}
if (swapSpace == null) {
throw new NullPointerException("swapSpace");
}
this.memoryCapacity = memoryCapacity;
this.swapSpace = swapSpace;
// try to get a prime number (more efficient?)
// lower values of LOAD_FACTOR increase speed, decrease space efficiency
cache = new Hashtable<Object, MemoryTile>(DEFAULT_HASHTABLE_CAPACITY, LOAD_FACTOR);
}
/**
* Adds a tile to the cache.
* <p/>
* <p> If the specified tile is already in the cache, it will not be
* cached again. If by adding this tile, the cache exceeds the memory
* capacity, older tiles in the cache are removed to keep the cache
* memory usage under the specified limit.
*
* @param owner The image the tile blongs to.
* @param tileX The tile's X index within the image.
* @param tileY The tile's Y index within the image.
* @param tile The tile to be cached.
*/
public void add(RenderedImage owner,
int tileX,
int tileY,
Raster tile) {
add(owner, tileX, tileY, tile, null);
}
/**
* Adds a tile to the cache with an associated tile compute cost.
* <p/>
* <p> If the specified tile is already in the cache, it will not be
* cached again. If by adding this tile, the cache exceeds the memory
* capacity, older tiles in the cache are removed to keep the cache
* memory usage under the specified limit.
*
* @param owner The image the tile blongs to.
* @param tileX The tile's X index within the image.
* @param tileY The tile's Y index within the image.
* @param tile The tile to be cached.
* @param tileCacheMetric Metric for prioritizing tiles
*/
public synchronized void add(RenderedImage owner,
int tileX,
int tileY,
Raster tile,
Object tileCacheMetric) {
if (memoryCapacity == 0) {
return;
}
addTileNonSync(owner, tileX, tileY, tile, tileCacheMetric);
}
/**
* Adds an array of tiles to the tile cache.
*
* @param owner The <code>RenderedImage</code> that the tile belongs to.
* @param tileIndices An array of <code>Point</code>s containing the
* <code>tileX</code> and <code>tileY</code> indices for each tile.
* @param tiles The array of tile <code>Raster</code>s containing tile data.
* @param tileCacheMetric Object which provides an ordering metric
* associated with the <code>RenderedImage</code> owner.
* @since 1.1
*/
public synchronized void addTiles(RenderedImage owner,
Point[] tileIndices,
Raster[] tiles,
Object tileCacheMetric) {
if (memoryCapacity == 0) {
return;
}
for (int i = 0; i < tileIndices.length; i++) {
int tileX = tileIndices[i].x;
int tileY = tileIndices[i].y;
Raster tile = tiles[i];
addTileNonSync(owner, tileX, tileY, tile, tileCacheMetric);
}
}
private void addTileNonSync(RenderedImage owner, int tileX, int tileY, Raster tile, Object tileCacheMetric) {
Object key = MemoryTile.hashKey(owner, tileX, tileY);
MemoryTile ct = cache.get(key);
if (ct != null) {
ct.timeStamp = timeStamp++;
if (ct != first) {
// Bring this tile to the beginning of the list.
if (ct == last) {
last = ct.previous;
last.next = null;
} else {
ct.previous.next = ct.next;
ct.next.previous = ct.previous;
}
ct.previous = null;
ct.next = first;
first.previous = ct;
first = ct;
}
hitCount++;
if (diagnostics) {
ct.action = UPDATE_FROM_ADD;
setChanged();
notifyObservers(ct);
}
} else {
ct = new MemoryTile(owner, tileX, tileY, tile, tileCacheMetric);
addTileNonSync(ct);
}
}
private boolean addTileNonSync(MemoryTile ct) {
// Don't cache tile if adding it would provoke memoryControl()
// which would in turn only end up removing the tile.
if (memoryUsage + ct.tileSize > memoryCapacity &&
ct.tileSize > (long) (memoryCapacity * memoryThreshold)) {
return false;
}
ct.timeStamp = timeStamp++;
ct.previous = null;
ct.next = first;
if (first == null && last == null) {
first = ct;
last = ct;
} else {
if (first == null) {
throw new IllegalStateException("first == null");
}
first.previous = ct;
first = ct; // put this tile at the top of the list
}
// add to tile cache
if (cache.put(ct.key, ct) == null) {
memoryUsage += ct.tileSize;
tileCount++;
//missCount++; Not necessary?
if (cacheSortedSet != null) {
cacheSortedSet.add(ct);
}
if (diagnostics) {
ct.action = ADD;
setChanged();
notifyObservers(ct);
}
}
// Bring memory usage down to memoryThreshold % of memory capacity.
if (memoryUsage > memoryCapacity) {
memoryControl();
}
return true;
}
/**
* Removes a tile from the cache.
* <p/>
* <p> If the specified tile is not in the cache, this method
* does nothing.
*/
public synchronized void remove(RenderedImage owner,
int tileX,
int tileY) {
if (memoryCapacity == 0) {
return;
}
removeNonSync(owner, tileX, tileY);
}
/**
* Removes all the tiles that belong to a <code>RenderedImage</code>
* from the cache.
*
* @param owner The image whose tiles are to be removed from the cache.
*/
public synchronized void removeTiles(RenderedImage owner) {
if (memoryCapacity == 0) {
return;
}
int minTx = owner.getMinTileX();
int minTy = owner.getMinTileY();
int maxTx = minTx + owner.getNumXTiles();
int maxTy = minTy + owner.getNumYTiles();
for (int y = minTy; y < maxTy; y++) {
for (int x = minTx; x < maxTx; x++) {
removeNonSync(owner, x, y);
}
}
}
private void removeNonSync(RenderedImage owner, int tileX, int tileY) {
Object key = MemoryTile.hashKey(owner, tileX, tileY);
MemoryTile ct = cache.get(key);
if (ct != null) {
// Notify observers that a tile is about to be removed.
// It is possible that the tile will be removed from the
// cache before the observers get notified. This should
// be ok, since a hard reference to the tile will be
// kept for the observers, so the garbage collector won't
// remove the tile until the observers release it.
ct.action = ABOUT_TO_REMOVE;
setChanged();
notifyObservers(ct);
ct = cache.remove(key);
// recalculate memoryUsage only if tile is actually removed
if (ct != null) {
memoryUsage -= ct.tileSize;
tileCount--;
if (cacheSortedSet != null) {
cacheSortedSet.remove(ct);
}
if (ct == first) {
if (ct == last) {
first = null; // only one tile in the list
last = null;
} else {
first = ct.next;
first.previous = null;
}
} else if (ct == last) {
last = ct.previous;
last.next = null;
} else {
ct.previous.next = ct.next;
ct.next.previous = ct.previous;
}
// Notify observers that a tile has been removed.
// If the core's hard references go away, the
// soft references will be garbage collected.
// Usually, by the time the observers are notified
// the ct owner and tile are nulled by the GC, so
// we can't really tell which op was removed
// This occurs when OpImage's finalize method is
// invoked. This code works ok when remove is
// called directly. (by flush() for example).
// If the soft references are GC'd, the timeStamp
// will no longer be contiguous, it will be
// unique, so this is ok.
if (diagnostics) {
ct.action = REMOVE;
setChanged();
notifyObservers(ct);
}
ct.previous = null;
ct.next = null;
}
}
// <NEW>
swapSpace.deleteTile(owner, tileX, tileY);
// </NEW>
}
/**
* Retrieves a tile from the cache.
* <p/>
* <p> If the specified tile is not in the cache, this method
* returns <code>null</code>. If the specified tile is in the
* cache, its last-access time is updated.
*
* @param owner The image the tile blongs to.
* @param tileX The tile's X index within the image.
* @param tileY The tile's Y index within the image.
*/
public synchronized Raster getTile(RenderedImage owner,
int tileX,
int tileY) {
if (memoryCapacity == 0) {
return null;
}
return getTileNonSync(owner, tileX, tileY);
}
/**
* Retrieves a contiguous array of all tiles in the cache which are
* owned by the specified image. May be <code>null</code> if there
* were no tiles in the cache. The array contains no null entries.
*
* @param owner The <code>RenderedImage</code> to which the tiles belong.
* @return An array of all tiles owned by the specified image or
* <code>null</code> if there are none currently in the cache.
*/
public synchronized Raster[] getTiles(RenderedImage owner) {
if (memoryCapacity == 0) {
return null;
}
int size = Math.min(owner.getNumXTiles() * owner.getNumYTiles(),
(int) tileCount);
if (size > 0) {
int minTx = owner.getMinTileX();
int minTy = owner.getMinTileY();
int maxTx = minTx + owner.getNumXTiles();
int maxTy = minTy + owner.getNumYTiles();
ArrayList<Raster> temp = new ArrayList<Raster>(32);
for (int y = minTy; y < maxTy; y++) {
for (int x = minTx; x < maxTx; x++) {
Raster tile = getTileNonSync(owner, x, y);
if (tile != null) {
temp.add(tile);
}
}
}
if (!temp.isEmpty()) {
return temp.toArray(new Raster[temp.size()]);
}
}
return null;
}
/**
* Returns an array of tile <code>Raster</code>s from the cache.
* Any or all of the elements of the returned array may be <code>null</code>
* if the corresponding tile is not in the cache.
*
* @param owner The <code>RenderedImage</code> that the tile belongs to.
* @param tileIndices An array of <code>Point</code>s containing the
* <code>tileX</code> and <code>tileY</code> indices for each tile.
* @since 1.1
*/
public synchronized Raster[] getTiles(RenderedImage owner, Point[] tileIndices) {
if (memoryCapacity == 0) {
return null;
}
Raster[] tiles = new Raster[tileIndices.length];
for (int i = 0; i < tiles.length; i++) {
int tileX = tileIndices[i].x;
int tileY = tileIndices[i].y;
tiles[i] = getTileNonSync(owner, tileX, tileY);
}
return tiles;
}
private Raster getTileNonSync(RenderedImage owner, int tileX, int tileY) {
Object key = MemoryTile.hashKey(owner, tileX, tileY);
MemoryTile ct = cache.get(key);
Raster tile = null;
// <NEW>
if (ct == null) {
ct = swapSpace.restoreTile(owner, tileX, tileY);
if (ct != null) {
if (!addTileNonSync(ct)) {
return ct.getTile();
}
}
}
// </NEW>
if (ct == null) {
missCount++;
} else {
tile = ct.getTile();
// Update last-access time. (update() inlined for performance)
ct.timeStamp = timeStamp++;
if (ct != first) {
// Bring this tile to the beginning of the list.
if (ct == last) {
last = ct.previous;
last.next = null;
} else {
ct.previous.next = ct.next;
ct.next.previous = ct.previous;
}
ct.previous = null;
ct.next = first;
first.previous = ct;
first = ct;
}
hitCount++;
if (diagnostics) {
ct.action = UPDATE_FROM_GETTILE;
setChanged();
notifyObservers(ct);
}
}
return tile;
}
/**
* Removes -ALL- tiles from the cache.
*/
public synchronized void flush() {
//
// It is necessary to clear all the elements
// from the old cache in order to remove dangling
// references, due to the linked list. In other
// words, it is possible to reache the object
// through 2 paths so the object does not
// become weakly reachable until the reference
// to it in the hash map is null. It is not enough
// to just set the object to null.
//
Enumeration keys = cache.keys(); // all keys in Hashtable
// reset counters before diagnostics
hitCount = 0;
missCount = 0;
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
MemoryTile ct = cache.remove(key);
// recalculate memoryUsage only if tile is actually removed
if (ct != null) {
memoryUsage -= ct.tileSize;
tileCount--;
if (ct == first) {
if (ct == last) {
first = null; // only one tile in the list
last = null;
} else {
first = ct.next;
first.previous = null;
}
} else if (ct == last) {
last = ct.previous;
last.next = null;
} else {
ct.previous.next = ct.next;
ct.next.previous = ct.previous;
}
ct.previous = null;
ct.next = null;
// diagnostics
if (diagnostics) {
ct.action = REMOVE_FROM_FLUSH;
setChanged();
notifyObservers(ct);
}
}
}
if (memoryCapacity > 0) {
cache = new Hashtable<Object, MemoryTile>(DEFAULT_HASHTABLE_CAPACITY, LOAD_FACTOR);
}
if (cacheSortedSet != null) {
cacheSortedSet.clear();
cacheSortedSet = createSortedSet();
}
// force reset after diagnostics
tileCount = 0;
timeStamp = 0;
memoryUsage = 0;
// no System.gc() here, it's too slow and may occur anyway.
}
/**
* Returns the cache's tile capacity.
* <p/>
* <p> This implementation of <code>TileCache</code> does not use
* the tile capacity. This method always returns 0.
*/
public int getTileCapacity() {
return 0;
}
/**
* Sets the cache's tile capacity to the desired number of tiles.
* <p/>
* <p> This implementation of <code>TileCache</code> does not use
* the tile capacity. The cache size is limited by the memory
* capacity only. This method does nothing and has no effect on
* the cache.
*
* @param tileCapacity The desired tile capacity for this cache
* in number of tiles.
*/
public void setTileCapacity(int tileCapacity) {
}
/**
* Returns the cache's memory capacity in bytes.
*/
public long getMemoryCapacity() {
return memoryCapacity;
}
/**
* Sets the cache's memory capacity to the desired number of bytes.
* If the new memory capacity is smaller than the amount of memory
* currently being used by this cache, tiles are removed from the
* cache until the memory usage is less than the specified memory
* capacity.
*
* @param memoryCapacity The desired memory capacity for this cache
* in bytes.
* @throws IllegalArgumentException If <code>memoryCapacity</code>
* is less than 0.
*/
public void setMemoryCapacity(long memoryCapacity) {
if (memoryCapacity < 0) {
throw new IllegalArgumentException("memoryCapacity < 0");
} else if (memoryCapacity == 0) {
flush();
}
this.memoryCapacity = memoryCapacity;
if (memoryUsage > memoryCapacity) {
memoryControl();
}
}
/**
* Enable Tile Monitoring and Diagnostics
*/
public void enableDiagnostics() {
diagnostics = true;
}
/**
* Turn off diagnostic notification
*/
public void disableDiagnostics() {
diagnostics = false;
}
public long getCacheTileCount() {
return tileCount;
}
public long getCacheMemoryUsed() {
return memoryUsage;
}
public long getCacheHitCount() {
return hitCount;
}
public long getCacheMissCount() {
return missCount;
}
/**
* Reset hit and miss counters.
*
* @since 1.1
*/
public void resetCounts() {
hitCount = 0;
missCount = 0;
}
/**
* Set the memory threshold value.
*
* @since 1.1
*/
public void setMemoryThreshold(float mt) {
if (mt < 0.0F || mt > 1.0F) {
throw new IllegalArgumentException("mt < 0.0F || mt > 1.0F");
} else {
memoryThreshold = mt;
memoryControl();
}
}
/**
* Returns the current <code>memoryThreshold</code>.
*
* @since 1.1
*/
public float getMemoryThreshold() {
return memoryThreshold;
}
/**
* Returns a string representation of the class object.
*/
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode()) +
": memoryCapacity = " + Long.toHexString(memoryCapacity) +
" memoryUsage = " + Long.toHexString(memoryUsage) +
" #tilesInCache = " + Integer.toString(cache.size());
}
/**
* @return the <code>Object</code> that represents the actual cache.
*/
public Object getCachedObject() {
return cache;
}
/**
* Removes tiles from the cache based on their last-access time
* (old to new) until the memory usage is memoryThreshold % of that of the
* memory capacity.
*/
public synchronized void memoryControl() {
if (cacheSortedSet == null) {
standard_memory_control();
} else {
custom_memory_control();
}
}
// time stamp based memory control (LRU)
private void standard_memory_control() {
long limit = (long) (memoryCapacity * memoryThreshold);
while (memoryUsage > limit && last != null) {
MemoryTile ct = cache.get(last.key);
if (ct != null) {
ct = cache.remove(last.key);
// <NEW>
swapSpace.storeTile(ct);
// </NEW>
memoryUsage -= last.tileSize;
tileCount--;
last = last.previous;
if (last != null) {
last.next.previous = null;
last.next = null;
} else {
first = null;
}
// diagnostics
if (diagnostics) {
ct.action = REMOVE_FROM_MEMCON;
setChanged();
notifyObservers(ct);
}
}
}
}
// comparator based memory control (TreeSet)
private void custom_memory_control() {
long limit = (long) (memoryCapacity * memoryThreshold);
Iterator iter = cacheSortedSet.iterator();
MemoryTile ct;
while (iter.hasNext() && (memoryUsage > limit)) {
ct = (MemoryTile) iter.next();
memoryUsage -= ct.tileSize;
tileCount--;
// remove from sorted set
try {
iter.remove();
} catch (ConcurrentModificationException e) {
ImagingListener listener =
ImageUtil.getImagingListener((RenderingHints) null);
listener.errorOccurred("Concurrent modification of tile list.",
e, this, false);
// e.printStackTrace();
}
// remove tile from the linked list
if (ct == first) {
if (ct == last) {
first = null;
last = null;
} else {
first = ct.next;
if (first != null) {
first.previous = null;
first.next = ct.next.next;
}
}
} else if (ct == last) {
last = ct.previous;
if (last != null) {
last.next = null;
last.previous = ct.previous.previous;
}
} else {
MemoryTile ptr = first.next;
while (ptr != null) {
if (ptr == ct) {
if (ptr.previous != null) {
ptr.previous.next = ptr.next;
}
if (ptr.next != null) {
ptr.next.previous = ptr.previous;
}
break;
}
ptr = ptr.next;
}
}
// remove reference in the hashtable
cache.remove(ct.key);
// <NEW>
swapSpace.storeTile(ct);
// </NEW>
// diagnostics
if (diagnostics) {
ct.action = REMOVE_FROM_MEMCON;
setChanged();
notifyObservers(ct);
}
}
// If the custom memory control didn't release sufficient
// number of tiles to satisfy the memory limit, fallback
// to the standard memory controller.
if (memoryUsage > limit) {
standard_memory_control();
}
}
/**
* The <code>Comparator</code> is used to produce an
* ordered list of tiles based on a user defined
* compute cost or priority metric. This determines
* which tiles are subject to "ordered" removal
* during a memory control operation.
*
* @since 1.1
*/
public synchronized void setTileComparator(Comparator c) {
comparator = c;
if (comparator == null) {
// turn of comparator
if (cacheSortedSet != null) {
cacheSortedSet.clear();
cacheSortedSet = null;
}
} else {
// copy tiles from hashtable to sorted tree set
cacheSortedSet = createSortedSet();
Enumeration keys = cache.keys();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
MemoryTile ct = cache.get(key);
cacheSortedSet.add(ct);
}
}
}
/**
* Return the current comparator
*
* @since 1.1
*/
public Comparator getTileComparator() {
return comparator;
}
// test
public void dump() {
System.out.println("first = " + first);
System.out.println("last = " + last);
Iterator iter = cacheSortedSet.iterator();
int k = 0;
while (iter.hasNext()) {
MemoryTile ct = (MemoryTile) iter.next();
System.out.println(k++);
System.out.println(ct);
}
}
void sendExceptionToListener(String message, Exception e) {
ImagingListener listener =
ImageUtil.getImagingListener((RenderingHints) null);
listener.errorOccurred(message, e, this, false);
}
SortedSet<MemoryTile> createSortedSet() {
// noinspection unchecked
return Collections.synchronizedSortedSet(new TreeSet<MemoryTile>(comparator));
}
}