/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2003-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotools.gui.swing.image;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTabbedPane;
import javax.swing.JScrollPane;
import javax.swing.table.AbstractTableModel;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Dimension;
import java.lang.reflect.Array;
import java.awt.Image;
import java.awt.image.DataBuffer;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.renderable.RenderableImage;
import javax.media.jai.OperationNode;
import javax.media.jai.PropertySource;
import javax.media.jai.PropertyChangeEmitter;
import javax.media.jai.RegistryElementDescriptor;
import javax.media.jai.OperationDescriptor;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
/**
* A panel showing image properties. An image can actually be any instance of
* {@link PropertySource}, {@link RenderedImage} or {@link RenderableImage} interfaces.
* The method {@link PropertySource#getProperty} will be invoked only when a property
* is first required, in order to avoid the computation of deferred properties before
* needed. If the source implements also the {@link PropertyChangeEmitter} interface,
* then this widget will register a listener for property changes. The changes can be
* emitted from any thread, which may or may not be the <cite>Swing</cite> thread.
* <p>
* If the image is an instance of {@link RenderedImage}, then this panel will also show
* informations about the {@linkplain ColorModel color model}, {@linkplain SampleModel
* sample model}, image size, tile size, etc.
*
* @since 2.3
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @see org.geotools.gui.swing.ParameterEditor
* @see OperationTreeBrowser
*/
public class ImageProperties extends JPanel {
/**
* The operation name, or the image class name if the image is not an instance of
* {@link OperationNode}.
*/
private final JLabel operationName = new JLabel(" ");
/**
* The operation description.
*/
private final JLabel operationDescription = new JLabel(" ");
/**
* The operation vendor and version.
*/
private final JLabel operationVersion = new JLabel(" ");
/**
* The text area for image size.
*/
private final JLabel imageSize = new JLabel();
/**
* The text area for tile size.
*/
private final JLabel tileSize = new JLabel();
/**
* The text area for sample type (e.g. "8 bits unsigned integer".
*/
private final JLabel dataType = new JLabel();
/**
* The text area for the sample model.
*/
private final JLabel sampleModel = new JLabel();
/**
* The text area for the color model.
*/
private final JLabel colorModel = new JLabel();
/**
* The color bar for {@link IndexColorModel}.
*/
private final ColorRamp colorRamp = new ColorRamp();
/**
* The table model for image's properties.
*/
private final Table properties;
/**
* The table for sample values.
*/
private final ImageSampleValues samples;
/**
* The viewer for an image quick look.
*/
private final ImagePane viewer;
/**
* Create a new instance of {@code ImageProperties} with no image.
* One of {@link #setImage(PropertySource) setImage(...)} methods must
* be invoked in order to set the property source.
*/
public ImageProperties() {
super(new BorderLayout());
final Vocabulary resources = Vocabulary.getResources(getLocale());
final JTabbedPane tabs = new JTabbedPane();
final GridBagConstraints c = new GridBagConstraints();
/*
* Build the informations tab.
*/
if (true) {
final JPanel panel = new JPanel(new GridBagLayout());
c.anchor=c.WEST; c.fill=c.HORIZONTAL; c.insets.left=9;
c.gridx=0; c.gridwidth=2; c.weightx=1;
c.gridy=0; panel.add(operationName, c);
c.gridy++; panel.add(operationDescription, c); c.insets.bottom=15;
c.gridy++; panel.add(operationVersion, c);
final int ytop = c.gridy;
c.gridwidth=1; c.weightx=0; c.insets.bottom=0;
c.gridy++; panel.add(getLabel(VocabularyKeys.IMAGE_SIZE, resources), c);
c.gridy++; panel.add(getLabel(VocabularyKeys.TILES_SIZE, resources), c);
c.gridy++; panel.add(getLabel(VocabularyKeys.DATA_TYPE, resources), c);
c.gridy++; panel.add(getLabel(VocabularyKeys.SAMPLE_MODEL, resources), c);
c.gridy++; panel.add(getLabel(VocabularyKeys.COLOR_MODEL, resources), c);
c.gridy++; panel.add(getLabel(VocabularyKeys.COLORS, resources), c);
c.gridx=1; c.gridy=ytop; c.weightx=1;
c.gridy++; panel.add(imageSize, c);
c.gridy++; panel.add(tileSize, c);
c.gridy++; panel.add(dataType, c);
c.gridy++; panel.add(sampleModel, c);
c.gridy++; panel.add(colorModel, c);
c.gridy++; c.anchor=c.CENTER; c.insets.right=6;
panel.add(colorRamp, c);
tabs.addTab(resources.getString(VocabularyKeys.INFORMATIONS), panel);
}
/*
* Build the image's properties tab.
*/
if (true) {
properties = new Table(resources);
final JTable table = new JTable(properties);
tabs.addTab(resources.getString(VocabularyKeys.PROPERTIES), new JScrollPane(table));
}
/*
* Build the image sample value tab.
*/
if (true) {
samples = new ImageSampleValues();
tabs.addTab(resources.getString(VocabularyKeys.PIXELS), samples);
}
/*
* Build the image preview tab.
*/
if (true) {
viewer = new ImagePane();
viewer.setPaintingWhileAdjusting(true);
tabs.addTab(resources.getString(VocabularyKeys.PREVIEW), viewer.createScrollPane());
}
add(tabs, BorderLayout.CENTER);
setPreferredSize(new Dimension(400,250));
}
/**
* Returns the localized label for the given key.
*/
private static JLabel getLabel(final int key, final Vocabulary resources) {
return new JLabel(resources.getLabel(key));
}
/**
* Create a new instance of {@code ImageProperties} for the specified
* rendered image.
*
* @param image The image, or {@code null} if none.
*/
public ImageProperties(final RenderedImage image) {
this();
if (image != null) {
setImage(image);
}
}
/**
* Set the operation name, description and version for the given image. If the image is
* an instance of {@link OperationNode}, then a description of the operation will be fetch
* from its resources bundle.
*
* @param image The image, or {@code null} if none.
*/
private void setDescription(final Object image) {
String name = " ";
String description = " ";
String version = " ";
final Locale locale = getLocale();
final Vocabulary resources = Vocabulary.getResources(locale);
if (image instanceof OperationNode) {
final String mode;
final RegistryElementDescriptor descriptor;
final OperationNode operation = (OperationNode) image;
name = operation.getOperationName();
mode = operation.getRegistryModeName();
descriptor = operation.getRegistry().getDescriptor(mode, name);
if (descriptor instanceof OperationDescriptor) {
final ResourceBundle bundle;
bundle = ((OperationDescriptor) descriptor).getResourceBundle(locale);
name = bundle .getString("LocalName");
description = bundle .getString("Description");
version = resources.getString(VocabularyKeys.VERSION_$1,
bundle .getString("Version")) + ", " +
bundle .getString("Vendor");
name = resources.getString(VocabularyKeys.OPERATION_$1, name);
}
} else if (image != null) {
name = Classes.getShortClassName(image);
name = resources.getString(VocabularyKeys.IMAGE_CLASS_$1, name);
}
operationName .setText(name );
operationDescription.setText(description);
operationVersion .setText(version );
}
/**
* Set all text fields to {@code null}. This method do not set the {@link #properties}
* table; this is left to the caller.
*/
private void clear() {
imageSize .setText(null);
tileSize .setText(null);
dataType .setText(null);
sampleModel.setText(null);
colorModel .setText(null);
colorRamp .setColors((IndexColorModel)null);
}
/**
* Set the {@linkplain PropertySource property source} for this widget. If the source is a
* {@linkplain RenderedImage rendered} or a {@linkplain RenderableImage renderable} image,
* then the widget will be set as if the most specific flavor of {@code setImage(...)}
* was invoked.
*
* @param image The image, or {@code null} if none.
*/
public void setImage(final PropertySource image) {
if (image instanceof RenderedImage) {
setImage((RenderedImage) image);
return;
}
if (image instanceof RenderableImage) {
setImage((RenderableImage) image);
return;
}
clear();
setDescription(image);
properties.setSource(image);
viewer .setImage((RenderedImage) null);
samples .setImage((RenderedImage) null);
}
/**
* Set the specified {@linkplain RenderableImage renderable image} as the properties source.
*
* @param image The image, or {@code null} if none.
*/
public void setImage(final RenderableImage image) {
clear();
if (image != null) {
final Vocabulary resources = Vocabulary.getResources(getLocale());
imageSize.setText(resources.getString(VocabularyKeys.SIZE_$2,
new Float(image.getWidth()),
new Float(image.getHeight())));
}
setDescription(image);
properties.setSource(image);
viewer .setImage (image);
samples .setImage ((RenderedImage) null);
}
/**
* Set the specified {@linkplain RenderedImage rendered image} as the properties source.
*
* @param image The image, or {@code null} if none.
*/
public void setImage(final RenderedImage image) {
if (image == null) {
clear();
} else {
final Vocabulary resources = Vocabulary.getResources(getLocale());
final ColorModel cm = image.getColorModel();
final SampleModel sm = image.getSampleModel();
imageSize.setText(resources.getString(VocabularyKeys.IMAGE_SIZE_$3,
new Integer(image.getWidth()),
new Integer(image.getHeight()),
new Integer(sm.getNumBands())));
tileSize.setText(resources.getString(VocabularyKeys.TILE_SIZE_$4,
new Integer(image.getNumXTiles()),
new Integer(image.getNumYTiles()),
new Integer(image.getTileWidth()),
new Integer(image.getTileHeight())));
dataType .setText(getDataType(sm.getDataType(), cm, resources));
sampleModel.setText(formatClassName(sm, resources));
colorModel .setText(formatClassName(cm, resources));
if (cm instanceof IndexColorModel) {
colorRamp.setColors((IndexColorModel) cm);
} else {
colorRamp.setColors((IndexColorModel) null);
}
}
setDescription(image);
properties.setSource(image);
viewer .setImage (image);
samples .setImage (image);
}
/**
* Returns a string representation for the given data type.
*
* @param type The data type (one of {@link DataBuffer} constants).
* @param cm The color model, for computing the pixel size in bits.
* @param resources The resources to use for formatting the type.
* @return The data type as a localized string.
*/
private static String getDataType(final int type,
final ColorModel cm,
final Vocabulary resources)
{
final int key;
switch (type) {
case DataBuffer.TYPE_BYTE: // Fall through
case DataBuffer.TYPE_USHORT: key=VocabularyKeys.UNSIGNED_INTEGER_$2; break;
case DataBuffer.TYPE_SHORT: // Fall through
case DataBuffer.TYPE_INT: key=VocabularyKeys.SIGNED_INTEGER_$1; break;
case DataBuffer.TYPE_FLOAT: // Fall through
case DataBuffer.TYPE_DOUBLE: key=VocabularyKeys.REAL_NUMBER_$1; break;
case DataBuffer.TYPE_UNDEFINED: // Fall through
default: return resources.getString(VocabularyKeys.UNDEFINED);
}
final Integer typeSize = new Integer(DataBuffer.getDataTypeSize(type));
final Integer pixelSize = (cm!=null) ? new Integer(cm.getPixelSize()) : typeSize;
return resources.getString(key, typeSize, pixelSize);
}
/**
* Split a class name into a more human readeable sentence
* (e.g. "PixelInterleavedSampleModel" into "Pixel interleaved sample model").
*
* @param object The object to format.
* @param resources The resources to use for formatting localized text.
* @return The object class name.
*/
private static String formatClassName(final Object object, final Vocabulary resources) {
if (object == null) {
return resources.getString(VocabularyKeys.UNDEFINED);
}
final String name = Classes.getShortClassName(object);
final int length = name.length();
final StringBuilder buffer = new StringBuilder(length + 8);
int last = 0;
for (int i=1; i<=length; i++) {
if (i==length ||
(Character.isUpperCase(name.charAt(i )) &&
Character.isLowerCase(name.charAt(i-1))))
{
final int pos = buffer.length();
buffer.append(name.substring(last, i));
buffer.append(' ');
if (pos!=0 && last<length-1 && Character.isLowerCase(name.charAt(last+1))) {
buffer.setCharAt(pos, Character.toLowerCase(buffer.charAt(pos)));
}
last = i;
}
}
if (object instanceof IndexColorModel) {
final IndexColorModel cm = (IndexColorModel) object;
buffer.append(" (").append(resources.getString(VocabularyKeys.COLOR_COUNT_$1, cm.getMapSize()));
buffer.append(')');
}
return buffer.toString().trim();
}
/**
* The table model for image's properties. The image can actually be any of
* {@link PropertySource}, {@link RenderedImage} or {@link RenderableImage}
* interface. The method {@link PropertySource#getProperty} will be invoked
* only when a property is first required, in order to avoid the computation
* of deferred properties before needed. If the source implements also the
* {@link PropertyChangeEmitter} interface, then this table will be registered
* as a listener for property changes. The changes can be emitted from any thread,
* which may or may not be the <cite>Swing</cite> thread.
*
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @todo Check for {@code WritablePropertySource} and make cells editable accordingly.
*/
private static final class Table extends AbstractTableModel implements PropertyChangeListener {
/**
* The resources for formatting localized strings.
*/
private final Vocabulary resources;
/**
* The property sources. Usually (but not always) the same object than
* {@link #changeEmitter}. May be {@code null} if no source has been set.
*/
private PropertySource source;
/**
* The property change emitter, or {@code null} if none. Usually (but not always)
* the same object than {@link #source}.
*/
private PropertyChangeEmitter changeEmitter;
/**
* The properties names, or {@code null} if none.
*/
private String[] names;
/**
* Constructs a default table with no properties source. The method {@link #setSource}
* must be invoked after the construction in order to display some image's properties.
*
* @param resources The resources for formatting localized strings.
*/
public Table(final Vocabulary resources) {
this.resources = resources;
}
/**
* Wrap the specified {@link RenderedImage} into a {@link PropertySource}.
*/
private static PropertySource wrap(final RenderedImage image) {
return new PropertySource() {
public String[] getPropertyNames() {
return image.getPropertyNames();
}
public String[] getPropertyNames(final String prefix) {
// TODO: Not the real answer, but this method
// is not needed by this Table implementation.
return getPropertyNames();
}
public Class getPropertyClass(final String name) {
return null;
}
public Object getProperty(final String name) {
return image.getProperty(name);
}
};
}
/**
* Wrap the specified {@link RenderableImage} into a {@link PropertySource}.
*/
private static PropertySource wrap(final RenderableImage image) {
return new PropertySource() {
public String[] getPropertyNames() {
return image.getPropertyNames();
}
public String[] getPropertyNames(final String prefix) {
// TODO: Not the real answer, but this method
// is not needed by this Table implementation.
return getPropertyNames();
}
public Class getPropertyClass(final String name) {
return null;
}
public Object getProperty(final String name) {
return image.getProperty(name);
}
};
}
/**
* Set the source as a {@link PropertySource}, a {@link RenderedImage} or a
* {@link RenderableImage}. If the source implements the {@link PropertyChangeEmitter}
* interface, then this table will be registered as a listener for property changes.
* The changes can be emitted from any thread (may or may not be the Swing thread).
*
* @param image The properties source, or {@code null} for removing any source.
*/
public void setSource(final Object image) {
if (image == source) {
return;
}
if (changeEmitter != null) {
changeEmitter.removePropertyChangeListener(this);
changeEmitter = null;
}
if (image instanceof PropertySource) {
source = (PropertySource) image;
} else if (image instanceof RenderedImage) {
source = wrap((RenderedImage) image);
} else if (image instanceof RenderableImage) {
source = wrap((RenderableImage) image);
} else {
source = null;
}
names = (source!=null) ? source.getPropertyNames() : null;
if (image instanceof PropertyChangeEmitter) {
changeEmitter = (PropertyChangeEmitter) image;
changeEmitter.addPropertyChangeListener(this);
}
fireTableDataChanged();
}
/**
* Returns the number of rows, which is equals to the number of properties.
*/
public int getRowCount() {
return (names!=null) ? names.length : 0;
}
/**
* Returns the number of columns, which is 2 (the property name and its value).
*/
public int getColumnCount() {
return 2;
}
/**
* Returns the column name for the given index.
*/
public String getColumnName(final int column) {
final int key;
switch (column) {
case 0: key=VocabularyKeys.NAME; break;
case 1: key=VocabularyKeys.VALUE; break;
default: throw new IndexOutOfBoundsException(String.valueOf(column));
}
return resources.getString(key);
}
/**
* Returns the most specific superclass for all the cell values in the column.
*/
public Class getColumnClass(final int column) {
switch (column) {
case 0: return String.class;
case 1: return Object.class;
default: throw new IndexOutOfBoundsException(String.valueOf(column));
}
}
/**
* Returns the property for the given cell.
*
* @param row The row index.
* @param column The column index.
* @return The cell value at the given index.
* @throws IndexOutOfBoundsException if the row or the column is out of bounds.
*/
public Object getValueAt(int row, int column) throws IndexOutOfBoundsException {
final String name = names[row];
switch (column) {
case 0: {
return name;
}
case 1: {
Object value = source.getProperty(name);
if (value == Image.UndefinedProperty) {
value = resources.getString(VocabularyKeys.UNDEFINED);
}
return expandArray(value);
}
default: {
throw new IndexOutOfBoundsException(String.valueOf(column));
}
}
}
/**
* If the specified object is an array, enumerate the array components.
* Otherwise, returns the object unchanged. This method is sligtly different
* than {@link java.util.Arrays#toString(Object[])} in that it expands inner
* array components recursively.
*/
private static Object expandArray(final Object array) {
if (array!=null && array.getClass().isArray()) {
final StringBuffer buffer = new StringBuffer();
buffer.append('{');
final int length = Array.getLength(array);
for (int i=0; i<length; i++) {
if (i != 0) {
buffer.append(", ");
}
buffer.append(expandArray(Array.get(array, i)));
}
buffer.append('}');
return buffer.toString();
}
return array;
}
/**
* Invoked when a property changed. This method find the row for the modified
* property and fire a table change event.
*
* @param The property change event.
*/
public void propertyChange(final PropertyChangeEvent event) {
/*
* Make sure that we are running in the Swing thread.
*/
if (!EventQueue.isDispatchThread()) {
EventQueue.invokeLater(new Runnable() {
public void run() {
propertyChange(event);
}
});
return;
}
/*
* Find the rows for the modified property, and fire a "table updated' event.
*/
final String name = event.getPropertyName();
int first = getRowCount(); // Past the last row.
int last = -1; // Before the first row.
if (name == null) {
last = first-1;
first = 0;
} else {
for (int i=first; --i>=0;) {
if (names[i].equalsIgnoreCase(name)) {
first = i;
if (last < 0) {
last = i;
}
}
}
}
if (first <= last) {
fireTableRowsUpdated(first, last);
}
}
}
}