/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.util;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.log4j.Logger;
import com.t3.MD5Key;
import com.t3.image.ImageUtil;
import com.t3.model.Asset;
import com.t3.model.AssetAvailableListener;
import com.t3.model.AssetManager;
/**
* The ImageManager class keeps a cache of loaded images. This class can be used to load the raw image data from an
* asset. The loading of the raw image data into a usable class is done in the background by one of two threads. The
* ImageManager will return a "?" (UNKNOWN_IMAGE) if the asset is still downloading or the asset image is still being
* loaded, and a "X" (BROKEN_IMAGE) if the asset or image is invalid. Small images are loaded using a different thread
* pool from large images, and allows small images to load quicker.
*/
public class ImageManager {
private static final Logger log = Logger.getLogger(ImageManager.class);
/** Cache of images loaded for assets. */
private static final Map<MD5Key, BufferedImage> imageMap = new HashMap<MD5Key, BufferedImage>();
/**
* The unknown image, a "?" is used for all situations where the image will eventually appear e.g. asset download,
* and image loading.
*/
private static final String UNKNOWN_IMAGE_PNG = "com/t3/client/image/unknown.png";
public static BufferedImage TRANSFERING_IMAGE;
/**
* The broken image, a "X" is used for all situations where the asset or image was invalid.
*/
private static final String BROKEN_IMAGE_PNG = "com/t3/client/image/broken.png";
public static BufferedImage BROKEN_IMAGE;
/**
* Small and large thread pools for background processing of asset raw image data.
*/
private static ExecutorService smallImageLoader = Executors.newFixedThreadPool(1);
private static ExecutorService largeImageLoader = Executors.newFixedThreadPool(1);
private static Object imageLoaderMutex = new Object();
/**
* A Map containing sets of observers for each asset id. Observers are notified when the image is done loading.
*/
private static Map<MD5Key, Set<ImageObserver>> imageObserverMap = new ConcurrentHashMap<MD5Key, Set<ImageObserver>>();
static {
try {
TRANSFERING_IMAGE = ImageUtil.getCompatibleImage(UNKNOWN_IMAGE_PNG);
} catch (IOException ioe) {
log.error("static for 'unknown.png': not resolved; IOException", ioe);
TRANSFERING_IMAGE = ImageUtil.createCompatibleImage(10, 10, 0);
}
try {
BROKEN_IMAGE = ImageUtil.getCompatibleImage(BROKEN_IMAGE_PNG);
} catch (IOException ioe) {
log.error("static for 'broken.png': not resolved; IOException", ioe);
BROKEN_IMAGE = ImageUtil.createCompatibleImage(10, 10, 0);
}
}
/**
* Remove all images from the image cache. The observers and image load hints are not flushed. The same observers
* will be notified when the image is reloaded, and the same hints will be used for loading.
*/
public static void flush() {
imageMap.clear();
}
/**
* Loads the asset's raw image data into a buffered image, and waits for the image to load.
*
* @param asset
* Load image data from this asset
* @return BufferedImage Return the loaded image
*/
public static BufferedImage getImageAndWait(MD5Key assetId) {
return getImageAndWait(assetId, null);
}
/**
* Flush all images that are <b>not</b> in the provided set. This presumes that the images in the exception set will
* still be in use after the flush.
*/
public static void flush(Set<MD5Key> exceptionSet) {
synchronized (imageLoaderMutex) {
for (MD5Key id : new HashSet<MD5Key>(imageMap.keySet())) {
if (!exceptionSet.contains(id)) {
imageMap.remove(id);
}
}
}
}
/**
* Loads the asset's raw image data into a buffered image, and waits for the image to load.
*
* @param asset
* Load image data from this asset
* @param hintMap
* Hints used when loading the image
* @return BufferedImage Return the loaded image
*/
public static BufferedImage getImageAndWait(final MD5Key assetId, Map<String, Object> hintMap) {
if (assetId == null) {
return BROKEN_IMAGE;
}
BufferedImage image = null;
final CountDownLatch loadLatch = new CountDownLatch(1);
image = getImage(assetId, new ImageObserver() {
@Override
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
// If we're here then the image has just finished loading
// release the blocked thread
log.debug("Countdown: " + assetId);
loadLatch.countDown();
return false;
}
});
if (image == TRANSFERING_IMAGE) {
try {
synchronized (loadLatch) {
log.debug("Wait for: " + assetId);
loadLatch.await();
}
// This time we'll get the cached version
image = getImage(assetId);
} catch (InterruptedException ie) {
log.error("getImageAndWait(" + assetId + "): image not resolved; InterruptedException", ie);
image = BROKEN_IMAGE;
}
}
return image;
}
public static BufferedImage getImage(MD5Key assetId, ImageObserver... observers) {
return getImage(assetId, null, observers);
}
public static BufferedImage getImage(MD5Key assetId, Map<String, Object> hints, ImageObserver... observers) {
if (assetId == null) {
return BROKEN_IMAGE;
}
synchronized (imageLoaderMutex) {
BufferedImage image = imageMap.get(assetId);
if (image != null && image != TRANSFERING_IMAGE) {
return image;
}
// Make note that we're currently processing it
imageMap.put(assetId, TRANSFERING_IMAGE);
// Make sure we are informed when it's done loading
addObservers(assetId, observers);
// Force a load of the asset, this will trigger a transfer if the
// asset is not available locally
if (image == null) {
AssetManager.getAssetAsynchronously(assetId, new AssetListener(assetId, hints));
}
return TRANSFERING_IMAGE;
}
}
/**
* Remove the image associated the asset from the cache.
*
* @param asset
* Asset associated with this image
*/
public static void flushImage(Asset asset) {
flushImage(asset.getId());
}
/**
* Remove the image associated this MD5Key from the cache.
*
* @param assetId
* MD5Key associated with this image
*/
public static void flushImage(MD5Key assetId) {
// LATER: investigate how this effects images that are already in progress
imageMap.remove(assetId);
}
/**
* Add observers, and associated hints for image loading, to be notified when the asset has completed loading.
*
* @param assetId
* Waiting for this asset to load
* @param hints
* Load the asset image with these hints
* @param observers
* Observers to be notified
*/
public static void addObservers(MD5Key assetId, ImageObserver... observers) {
if (observers == null || observers.length == 0) {
return;
}
Set<ImageObserver> observerSet = imageObserverMap.get(assetId);
if (observerSet == null) {
observerSet = new HashSet<ImageObserver>();
imageObserverMap.put(assetId, observerSet);
}
for (ImageObserver observer : observers) {
observerSet.add(observer);
}
}
/**
* Load the asset's raw image data into a BufferedImage.
*/
private static class BackgroundImageLoader implements Runnable {
private final Asset asset;
private final Map<String, Object> hints;
/**
* Create a background image loader to load the asset image using the hints provided.
*
* @param asset
* Asset to load
* @param hints
* Hints to use for image loading
*/
public BackgroundImageLoader(Asset asset, Map<String, Object> hints) {
this.asset = asset;
this.hints = hints;
}
/**
* Load the asset raw image data and notify observers that the image is loaded.
*/
@Override
public void run() {
log.debug("Loading asset: " + asset.getId());
BufferedImage image = imageMap.get(asset.getId());
if (image != null && image != TRANSFERING_IMAGE) {
// We've somehow already loaded this image
log.debug("Image wasn't in transit: " + asset.getId());
return;
}
try {
assert asset.getImage() != null : "asset.getImage() for " + asset.toString() + "returns null?!";
image = ImageUtil.createCompatibleImage(ImageUtil.bytesToImage(asset.getImage()), hints);
} catch (Throwable t) {
log.error("BackgroundImageLoader.run(" + asset.getName() + "," + asset.getId() + "): not resolved", t);
image = BROKEN_IMAGE;
}
synchronized (imageLoaderMutex) {
// Replace placeholder with actual image
imageMap.put(asset.getId(), image);
notifyObservers(asset, image);
}
}
}
/**
* Notify all observers watching the asset that the image is loaded.
*
* @param asset
* Loaded image from this asset
* @param image
* Result of loading the asset raw image data
*/
private static void notifyObservers(Asset asset, BufferedImage image) {
// Notify observers
log.debug("Notifying observers of image availability: " + asset.getId());
Set<ImageObserver> observerSet = imageObserverMap.remove(asset.getId());
if (observerSet != null) {
for (ImageObserver observer : observerSet) {
observer.imageUpdate(image, ImageObserver.ALLBITS, 0, 0, image.getWidth(), image.getHeight());
}
}
}
/**
* Run a thread to load the asset raw image data in the background using the provided hints.
*
* @param asset
* Load raw image data from this asset
* @param hints
* Hints used when loading image data
*/
private static void backgroundLoadImage(Asset asset, Map<String, Object> hints) {
// Use large image loader if the image is larger than 128kb.
if (asset.getImage().length > 128 * 1024) {
largeImageLoader.execute(new BackgroundImageLoader(asset, hints));
} else {
smallImageLoader.execute(new BackgroundImageLoader(asset, hints));
}
}
private static class AssetListener implements AssetAvailableListener {
private final MD5Key id;
private final Map<String, Object> hints;
public AssetListener(MD5Key id, Map<String, Object> hints) {
this.id = id;
this.hints = hints;
}
@Override
public void assetAvailable(MD5Key key) {
if (!key.equals(id)) {
return;
}
// No longer need to be notified when this asset is available
AssetManager.removeAssetListener(id, this);
// Image is now available for loading
log.debug("Asset available: " + id);
backgroundLoadImage(AssetManager.getAsset(id), hints);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
return id.equals(obj);
}
}
}