package prefuse.render;
import java.awt.Component;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import prefuse.data.Tuple;
import prefuse.util.io.IOLib;
/**
* <p>Utility class that manages loading and storing images. Includes a
* configurable LRU cache for managing loaded images. Also supports optional
* image scaling of loaded images to cut down on memory and visualization
* operation costs.</p>
*
* <p>By default images are loaded upon first request. Use the
* {@link #preloadImages(Iterator, String)} method to load images before they
* are requested for rendering.</p>
*
* @author alan newberger
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class ImageFactory {
protected int m_imageCacheSize = 3000;
protected int m_maxImageWidth = 100;
protected int m_maxImageHeight = 100;
protected boolean m_asynch = true;
//a nice LRU cache courtesy of java 1.4
protected Map imageCache =
new LinkedHashMap((int) (m_imageCacheSize + 1 / .75F), .75F, true) {
public boolean removeEldestEntry(Map.Entry eldest) {
return size() > m_imageCacheSize;
}
};
protected Map loadMap = new HashMap(50);
protected final Component component = new Component() {};
protected final MediaTracker tracker = new MediaTracker(component);
protected int nextTrackerID = 0;
/**
* Create a new ImageFactory. Assumes no scaling of loaded images.
*/
public ImageFactory() {
this(-1,-1);
}
/**
* Create a new ImageFactory. This instance will scale loaded images
* if they exceed the threshold arguments.
* @param maxImageWidth the maximum width of input images
* (-1 means no limit)
* @param maxImageHeight the maximum height of input images
* (-1 means no limit)
*/
public ImageFactory(int maxImageWidth, int maxImageHeight) {
setMaxImageDimensions(maxImageWidth, maxImageHeight);
}
/**
* Indicates if this ImageFactory loads images asynchronously (true by
* default)
* @return true for asynchronous (background) loading, false for
* synchronous (blocking) loading
*/
public boolean isAsynchronous() {
return m_asynch;
}
/**
* Sets if this ImageFactory loads images asynchronously.
* @param b true for asynchronous (background) loading, false for
* synchronous (blocking) loading
*/
public void setAsynchronous(boolean b) {
m_asynch = b;
}
/**
* Sets the maximum image dimensions of loaded images, images larger than
* these limits will be scaled to fit within bounds.
* @param width the maximum width of input images (-1 means no limit)
* @param height the maximum height of input images (-1 means no limit)
*/
public void setMaxImageDimensions(int width, int height) {
m_maxImageWidth = width;
m_maxImageHeight = height;
}
/**
* Sets the capacity of this factory's image cache
* @param size the new size of the image cache
*/
public void setImageCacheSize(int size) {
m_imageCacheSize = size;
}
/**
* Indicates if the given string location corresponds to an image
* currently stored in this ImageFactory's cache.
* @param imageLocation the image location string
* @return true if the location is a key for a currently cached image,
* false otherwise.
*/
public boolean isInCache(String imageLocation) {
return imageCache.containsKey(imageLocation);
}
/**
* <p>Get the image associated with the given location string. If the image
* has already been loaded, it simply will return the image, otherwise it
* will load it from the specified location.</p>
*
* <p>The imageLocation argument must be a valid resource string pointing
* to either (a) a valid URL, (b) a file on the classpath, or (c) a file
* on the local filesystem. The location will be resolved in that order.
* </p>
*
* @param imageLocation the image location as a resource string.
* @return the corresponding image, if available
*/
public Image getImage(String imageLocation) {
Image image = (Image) imageCache.get(imageLocation);
if (image == null && !loadMap.containsKey(imageLocation)) {
URL imageURL = IOLib.urlFromString(imageLocation);
if ( imageURL == null ) {
System.err.println("Null image: " + imageLocation);
return null;
}
image = Toolkit.getDefaultToolkit().createImage(imageURL);
// if set for synchronous mode, block for image to load.
if ( !m_asynch ) {
waitForImage(image);
addImage(imageLocation, image);
} else {
int id = ++nextTrackerID;
tracker.addImage(image, id);
loadMap.put(imageLocation, new LoadMapEntry(id,image));
}
} else if ( image == null && loadMap.containsKey(imageLocation) ) {
LoadMapEntry entry = (LoadMapEntry)loadMap.get(imageLocation);
if ( tracker.checkID(entry.id, true) ) {
addImage(imageLocation, entry.image);
loadMap.remove(imageLocation);
tracker.removeImage(entry.image, entry.id);
}
} else {
return image;
}
return (Image) imageCache.get(imageLocation);
}
/**
* Adds an image associated with a location string to this factory's cache.
* The image will be scaled as dictated by this current factory settings.
*
* @param location the location string uniquely identifying the image
* @param image the actual image
* @return the final image added to the cache. This may be a scaled version
* of the original input image.
*/
public Image addImage(String location, Image image) {
if ( m_maxImageWidth > -1 || m_maxImageHeight > -1 ) {
image = getScaledImage(image);
image.getWidth(null); // trigger image load
}
imageCache.put(location, image);
return image;
}
/**
* Wait for an image to load.
* @param image the image to wait for
*/
protected void waitForImage(Image image) {
int id = ++nextTrackerID;
tracker.addImage(image, id);
try {
tracker.waitForID(id, 0);
} catch (InterruptedException e) {
e.printStackTrace();
}
tracker.removeImage(image, id);
}
/**
* Scales an image to fit within the current size thresholds.
* @param img the image to scale
* @return the scaled image
*/
protected Image getScaledImage(Image img) {
// resize image, if necessary, to conserve memory
// and reduce future scaling time
int w = img.getWidth(null) - m_maxImageWidth;
int h = img.getHeight(null) - m_maxImageHeight;
if ( w > h && w > 0 && m_maxImageWidth > -1 ) {
Image scaled = img.getScaledInstance(m_maxImageWidth, -1, Image.SCALE_SMOOTH);
img.flush(); //waitForImage(scaled);
return scaled;
} else if ( h > 0 && m_maxImageHeight > -1 ) {
Image scaled = img.getScaledInstance(-1, m_maxImageHeight, Image.SCALE_SMOOTH);
img.flush(); //waitForImage(scaled);
return scaled;
} else {
return img;
}
}
/**
* <p>Preloads images for use in a visualization. Images to load are
* determined by taking objects from the given iterator and retrieving
* the value of the specified field. The items in the iterator must
* be instances of the {@link prefuse.data.Tuple} class.</p>
*
* <p>Images are loaded in the order specified by the iterator until the
* the iterator is empty or the maximum image cache size is met. Thus
* higher priority images should appear sooner in the iteration.</p>
*
* @param iter an Iterator of {@link prefuse.data.Tuple} instances
* @param field the data field that contains the image location
*/
public void preloadImages(Iterator iter, String field) {
boolean synch = m_asynch;
m_asynch = false;
String loc = null;
while ( iter.hasNext() && imageCache.size() <= m_imageCacheSize ) {
// get the string describing the image location
Tuple t = (Tuple)iter.next();
loc = t.getString(field);
if ( loc != null ) {
getImage(loc);
}
}
m_asynch = synch;
}
/**
* Helper class for storing an id/image pair.
*/
private class LoadMapEntry {
public int id;
public Image image;
public LoadMapEntry(int id, Image image) {
this.id = id;
this.image = image;
}
}
} // end of class ImageFactory