/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-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;
// J2SE dependencies
import java.awt.Color;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.NumberFormat;
import java.text.ParseException;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
/**
* A table model for image sample values (or pixels). This model is serialiable if the
* underlying {@link RenderedImage} is serializable.
*
* @since 2.3
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @todo Should supports deferred execution: request for a new tile should wait some maximal amount
* of time (e.g. 0.1 seconds). If the tile is not yet available after that time, the model
* should returns {@code null} at this time and send a "data changed" event later when the
* tile is finally available.
*
* @see ImageSampleValues
*/
public class ImageTableModel extends AbstractTableModel {
/**
* Serial number for compatibility with different versions.
*/
private static final long serialVersionUID = -408603520054548181L;
/**
* The image to display.
*/
private RenderedImage image;
/**
* The format to use for formatting sample values.
*/
private NumberFormat format = NumberFormat.getNumberInstance();
/**
* The format to use for formatting line and column labels.
*/
private NumberFormat titleFormat = NumberFormat.getIntegerInstance();
/**
* The band to show.
*/
private int band;
/**
* Image properites computed by {@link #update}. Those properties are used everytime
* {@link #getValueAt} is invoked, which is why we cache them.
*/
private transient int minX, minY, maxX, maxY,
tileGridXOffset, tileGridYOffset, tileWidth, tileHeight, dataType;
/**
* The type of sample values. Is computed by {@link #update}.
*/
private transient Class type = Number.class;
/**
* The row and column names. Will be created only when first needed.
*/
private transient String[] rowNames, columnNames;
/**
* The pixel values as an object of the color model transfert type.
* Cached for avoiding to much creation of the same object.
*/
private transient Object pixel;
/**
* Creates a new table model.
*/
public ImageTableModel() {
}
/**
* Creates a new table model for the specified image.
*/
public ImageTableModel(final RenderedImage image) {
setRenderedImage(image);
}
/**
* Returns the image to display, or {@code null} if none.
*/
public RenderedImage getRenderedImage() {
return image;
}
/**
* Sets the image to display.
*/
public void setRenderedImage(final RenderedImage image) {
this.image = image;
pixel = null;
rowNames = null;
columnNames = null;
final int digits = update();
format.setMinimumFractionDigits(digits);
format.setMaximumFractionDigits(digits);
fireTableStructureChanged();
}
/**
* Updates transient fields after an image change. Also invoked after deserialization.
* Returns the number of fraction digits to use for the format (to be ignored in the
* case of deserialization, since the format is serialized).
*/
private int update() {
int digits = 0;
if (image != null) {
minX = image.getMinX();
minY = image.getMinY();
maxX = image.getWidth() + minX;
maxY = image.getHeight() + minY;
tileGridXOffset = image.getTileGridXOffset();
tileGridYOffset = image.getTileGridYOffset();
tileWidth = image.getTileWidth();
tileHeight = image.getTileHeight();
dataType = image.getSampleModel().getDataType();
switch (dataType) {
case DataBuffer.TYPE_BYTE: // Fall through
case DataBuffer.TYPE_SHORT: // Fall through
case DataBuffer.TYPE_USHORT: // Fall through
case DataBuffer.TYPE_INT: type=Integer.class; break;
case DataBuffer.TYPE_FLOAT: type=Float .class; digits=2; break;
case DataBuffer.TYPE_DOUBLE: type=Double .class; digits=3; break;
default: type=Number .class; break;
}
} else {
type = Number.class;
}
return digits;
}
/**
* Recomputes transient fields after deserializations.
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
update();
}
/**
* Returns the band to display.
*/
public int getBand() {
return band;
}
/**
* Set the band to display.
*/
public void setBand(final int band) {
if (band<0 || (image!=null && band>=image.getSampleModel().getNumBands())) {
throw new IndexOutOfBoundsException();
}
this.band = band;
fireTableDataChanged();
}
/**
* Returns the format to use for formatting sample values.
*/
public NumberFormat getNumberFormat() {
return format;
}
/**
* Sets the format to use for formatting sample values.
*/
public void setNumberFormat(final NumberFormat format) {
this.format = format;
fireTableDataChanged();
}
/**
* Returns the number of rows in the model, which is
* the {@linkplain RenderedImage#getHeight image height}.
*/
public int getRowCount() {
return (image!=null) ? image.getHeight() : 0;
}
/**
* Returns the number of columns in the model, which is
* the {@linkplain RenderedImage#getWidth image width}.
*/
public int getColumnCount() {
return (image!=null) ? image.getWidth() : 0;
}
/**
* Returns the row name. The names are the pixel row number, starting at
* the {@linkplain RenderedImage#getMinY min y} value.
*/
public String getRowName(final int row) {
if (rowNames == null) {
rowNames = new String[image.getHeight()];
}
String candidate = rowNames[row];
if (candidate == null) {
rowNames[row] = candidate = titleFormat.format(minY + row);
}
return candidate;
}
/**
* Returns the column name. The names are the pixel column number, starting at
* the {@linkplain RenderedImage#getMinX min x} value.
*/
public String getColumnName(final int column) {
if (columnNames == null) {
if (image == null) {
return super.getColumnName(column);
}
columnNames = new String[image.getWidth()];
}
String candidate = columnNames[column];
if (candidate == null) {
columnNames[column] = candidate = titleFormat.format(minX + column);
}
return candidate;
}
/**
* Returns a column given its name.
*/
public int findColumn(final String name) {
if (image!=null) try {
return titleFormat.parse(name).intValue() - minX;
} catch (ParseException exception) {
// Ignore; fallback on the default algorithm.
}
return super.findColumn(name);
}
/**
* Returns the type of sample values regardless of column index.
*/
public Class getColumnClass(final int column) {
return type;
}
/**
* Returns the raster at the specified pixel location, or {@code null} if none.
* The (<var>x</var>, <var>y</var>) <strong>must</strong> be additionned with
* {@link #minX} and {@link #minY}.
*/
private final Raster getRasterAt(final int y, final int x) {
if (x<minX || x>=maxX || y<minY || y>=maxY) {
return null;
}
int tx = x-tileGridXOffset; if (x<0) tx += 1-tileWidth;
int ty = y-tileGridYOffset; if (y<0) ty += 1-tileHeight;
return image.getTile(tx/tileWidth, ty/tileHeight);
}
/**
* Returns the sample value at the specified row and column.
*/
public Object getValueAt(int y, int x) {
final Raster raster = getRasterAt(y+=minY, x+=minX);
if (raster == null) {
return null;
}
switch (dataType) {
default: return new Integer(raster.getSample (x,y,band));
case DataBuffer.TYPE_FLOAT: return new Float (raster.getSampleFloat (x,y,band));
case DataBuffer.TYPE_DOUBLE: return new Double (raster.getSampleDouble(x,y,band));
}
}
/**
* Returns the color at the specified row and column.
*/
public Color getColorAt(int y, int x) {
final Raster raster = getRasterAt(y+=minY, x+=minX);
if (raster == null) {
return null;
}
pixel = raster.getDataElements(x, y, pixel);
return new Color(image.getColorModel().getRGB(pixel), true);
}
/**
* A table model for row headers. This model has only one column, and each cell values
* is the {@linkplain ImageTableModel#getRowName row name} defined in the enclosing class.
* A table using this model can be set as the
* {@linkplain javax.swing.JScrollPane#setRowHeaderView scroll pane's row header} for an
* image table.
*
* @since 2.2
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @see javax.swing.JScrollPane#setRowHeader
*/
public class RowHeaders extends AbstractTableModel implements TableModelListener {
/**
* Serial number for compatibility with different versions.
*/
private static final long serialVersionUID = 5162324745024331522L;
/**
* Creates a new instance of row headers. This constructor immediately register
* the new instance as a listener of the enclosing {@link ImageTableModel}.
*/
public RowHeaders() {
ImageTableModel.this.addTableModelListener(this);
}
/**
* Returns the number of rows in the model. This is identical to
* the number of rows in the enclosing {@link ImageTableModel}.
*/
public int getRowCount() {
return ImageTableModel.this.getRowCount();
}
/**
* Returns the number of columns in the model, which is 1.
*/
public int getColumnCount() {
return 1;
}
/**
* Returns the type of row headers, which is {@code String.class}.
*/
public Class getColumnClass(final int column) {
return String.class;
}
/**
* Returns the row name for the given index, regardless of the column.
*/
public Object getValueAt(final int row, final int column) {
return getRowName(row);
}
/**
* Invoked when the enclosing {@link ImageTableModel} data changed. This method fires
* an event for this model as well except if the change was not a change in the table
* structure.
*/
public void tableChanged(final TableModelEvent event) {
final int firstRow = event.getFirstRow();
final int lastRow = event.getLastRow();
final int type = event.getType();
if (type!=TableModelEvent.UPDATE || lastRow==Integer.MAX_VALUE) {
fireTableChanged(new TableModelEvent(this, firstRow, lastRow, 0, type));
}
}
}
}