/*
* IconManager.java 2 mai 2006
*
* Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com>
*
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.eteks.sweethome3d.swing;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import com.eteks.sweethome3d.model.Content;
import com.eteks.sweethome3d.tools.ResourceURLContent;
/**
* Singleton managing icons cache.
* @author Emmanuel Puybaret
*/
public class IconManager {
private static IconManager instance;
// Icon used if an image content couldn't be loaded
private final Content errorIconContent;
// Icon used while an image content is loaded
private final Content waitIconContent;
// Map storing loaded icons
private final Map<Content, Map<Integer, Icon>> icons;
// Executor used by IconProxy to load images
private ExecutorService iconsLoader;
private IconManager() {
this.errorIconContent = new ResourceURLContent(IconManager.class, "resources/icons/tango/image-missing.png");
this.waitIconContent = new ResourceURLContent(IconManager.class, "resources/icons/tango/image-loading.png");
this.icons = Collections.synchronizedMap(new WeakHashMap<Content, Map<Integer, Icon>>());
}
/**
* Returns an instance of this singleton.
*/
public static IconManager getInstance() {
if (instance == null) {
instance = new IconManager();
}
return instance;
}
/**
* Clears the loaded resources cache and shutdowns the multithreaded service
* that loads icons.
*/
public void clear() {
if (this.iconsLoader != null) {
this.iconsLoader.shutdownNow();
this.iconsLoader = null;
}
this.icons.clear();
}
/**
* Returns the icon displayed for wrong content resized at a given height.
*/
public Icon getErrorIcon(int height) {
return getIcon(this.errorIconContent, height, null);
}
/**
* Returns the icon displayed for wrong content.
*/
public Icon getErrorIcon() {
return getIcon(this.errorIconContent, -1, null);
}
/**
* Returns <code>true</code> if the given <code>icon</code> is the error icon
* used by this manager to indicate it couldn't load an icon.
*/
public boolean isErrorIcon(Icon icon) {
Map<Integer, Icon> errorIcons = this.icons.get(this.errorIconContent);
return errorIcons != null
&& (errorIcons.containsValue(icon)
|| icon instanceof IconProxy
&& errorIcons.containsValue(((IconProxy)icon).getIcon()));
}
/**
* Returns the icon displayed while a content is loaded resized at a given height.
*/
public Icon getWaitIcon(int height) {
return getIcon(this.waitIconContent, height, null);
}
/**
* Returns the icon displayed while a content is loaded.
*/
public Icon getWaitIcon() {
return getIcon(this.waitIconContent, -1, null);
}
/**
* Returns <code>true</code> if the given <code>icon</code> is the wait icon
* used by this manager to indicate it's currently loading an icon.
*/
public boolean isWaitIcon(Icon icon) {
Map<Integer, Icon> waitIcons = this.icons.get(this.waitIconContent);
return waitIcons != null
&& (waitIcons.containsValue(icon)
|| icon instanceof IconProxy
&& waitIcons.containsValue(((IconProxy)icon).getIcon()));
}
/**
* Returns an icon read from <code>content</code>.
* @param content an object containing an image
* @param waitingComponent a waiting component. If <code>null</code>, the returned icon will
* be read immediately in the current thread.
*/
public Icon getIcon(Content content, Component waitingComponent) {
return getIcon(content, -1, waitingComponent);
}
/**
* Returns an icon read from <code>content</code> and rescaled at a given <code>height</code>.
* @param content an object containing an image
* @param height the desired height of the returned icon
* @param waitingComponent a waiting component. If <code>null</code>, the returned icon will
* be read immediately in the current thread.
*/
public Icon getIcon(Content content, final int height, Component waitingComponent) {
Map<Integer, Icon> contentIcons = this.icons.get(content);
if (contentIcons == null) {
contentIcons = Collections.synchronizedMap(new HashMap<Integer, Icon>());
this.icons.put(content, contentIcons);
}
Icon icon = contentIcons.get(height);
if (icon == null) {
// Tolerate null content
if (content == null) {
icon = new Icon() {
public void paintIcon(Component c, Graphics g, int x, int y) {
}
public int getIconWidth() {
return Math.max(0, height);
}
public int getIconHeight() {
return Math.max(0, height);
}
};
} else if (content == this.errorIconContent ||
content == this.waitIconContent) {
// Load error and wait icons immediately in this thread
icon = createIcon(content, height, null);
} else if (waitingComponent == null) {
// Load icon immediately in this thread
icon = createIcon(content, height,
getIcon(this.errorIconContent, height, null));
} else {
// For content different from error icon and wait icon,
// load it in a different thread with a virtual proxy
icon = new IconProxy(content, height, waitingComponent,
getIcon(this.errorIconContent, height, null),
getIcon(this.waitIconContent, height, null));
}
// Store the icon in icons map
contentIcons.put(height, icon);
}
return icon;
}
/**
* Returns an icon created and scaled from its content.
* @param content the content from which the icon image is read
* @param height the desired height of the returned icon
* @param errorIcon the returned icon in case of error
*/
private Icon createIcon(Content content, int height, Icon errorIcon) {
try {
// Read the icon of the piece
InputStream contentStream = content.openStream();
BufferedImage image = ImageIO.read(contentStream);
contentStream.close();
if (image != null) {
if (height != -1 && height != image.getHeight()) {
int width = image.getWidth() * height / image.getHeight();
// Create a scaled image not bound to original image to let the original image being garbage collected
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = scaledImage.getGraphics();
g.drawImage(image.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
g.dispose();
return new ImageIcon(scaledImage);
} else {
return new ImageIcon(image);
}
}
} catch (IOException ex) {
// Too bad, we'll use errorIcon
}
return errorIcon;
}
/**
* Proxy icon that displays a temporary icon while waiting
* image loading completion.
*/
private class IconProxy implements Icon {
private Icon icon;
public IconProxy(final Content content, final int height,
final Component waitingComponent,
final Icon errorIcon, Icon waitIcon) {
this.icon = waitIcon;
if (iconsLoader == null) {
iconsLoader = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
}
// Load the icon in a different thread
iconsLoader.execute(new Runnable () {
public void run() {
icon = createIcon(content, height, errorIcon);
waitingComponent.repaint();
}
});
}
public int getIconWidth() {
return this.icon.getIconWidth();
}
public int getIconHeight() {
return this.icon.getIconHeight();
}
public void paintIcon(Component c, Graphics g, int x, int y) {
this.icon.paintIcon(c, g, x, y);
}
public Icon getIcon() {
return this.icon;
}
}
}