/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.gui.swing.image;
import java.awt.Color;
import java.awt.image.Raster;
import java.awt.image.DataBuffer;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.NumberFormat;
import java.text.FieldPosition;
import java.text.ParseException;
import javax.swing.JScrollPane;
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 serializable if the
* underlying {@link RenderedImage} is serializable.
*
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @see ImageSampleValues
*
* @since 2.3
* @module
*
* @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.
*/
public class ImageTableModel extends AbstractTableModel {
/**
* Serial number for compatibility with different versions.
*/
private static final long serialVersionUID = -408603520054548181L;
/**
* The image to display, or {@code null} if none.
*/
private RenderedImage image;
/**
* The format to use for formatting sample values.
*/
private NumberFormat format;
/**
* The format to use for formatting line and column labels.
*/
private final NumberFormat titleFormat;
/**
* The band to show.
*/
private int band;
/**
* Image properties computed by {@link #update}. Those properties are used every time
* {@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<? extends Number> 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 transfer type.
* Cached for avoiding to much creation of the same object.
*/
private transient Object pixel;
/**
* Creates a new table model with no image.
*/
public ImageTableModel() {
format = NumberFormat.getNumberInstance();
titleFormat = NumberFormat.getIntegerInstance();
}
/**
* Creates a new table model for the specified image.
*
* @param image The image for which to create a table model, or {@code null} if none.
*/
public ImageTableModel(final RenderedImage image) {
this();
setRenderedImage(image);
}
/**
* Sets the image to display.
*
* @param image The new image for this table model, or {@code null} if none.
*/
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();
}
/**
* Returns the image to display, or {@code null} if none.
*
* @return The image which is backing this table model, or {@code null} if none.
*/
public RenderedImage getRenderedImage() {
return image;
}
/**
* 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.
*
* @return The band where this table model takes its values.
*/
public int getBand() {
return band;
}
/**
* Sets the band to display.
*
* @param band The band where this table model should take its values.
*/
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.
*
* @return The format used for formatting cell values.
*/
public NumberFormat getNumberFormat() {
return format;
}
/**
* Sets the format to use for formatting sample values.
*
* @param format The new format for formatting cell 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}.
*
* @return The image height, or 0 if there is no image.
*/
@Override
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}.
*
* @return The image width, or 0 if there is no image.
*/
@Override
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.
*
* @param row The row for which to get the name.
* @return The name for the requested row.
* @throws IndexOutOfBoundsException If the given index is not a positive number smaller
* than {@link #getRowCount()}.
*/
public String getRowName(final int row) throws IndexOutOfBoundsException {
if (rowNames == null) {
rowNames = new String[getRowCount()];
}
String candidate = rowNames[row];
if (candidate == null) {
rowNames[row] = candidate = titleFormat.format(minY + row,
new StringBuffer(), new FieldPosition(0)).append(" ").toString();
}
return candidate;
}
/**
* Returns the column name. The names are the pixel column number, starting at
* the {@linkplain RenderedImage#getMinX min x} value.
*
* @param column The column for which to get the name.
* @return The name for the requested column.
* @throws IndexOutOfBoundsException If the given index is not a positive number smaller
* than {@link #getColumnCount()}.
*/
@Override
public String getColumnName(final int column) throws IndexOutOfBoundsException {
if (columnNames == null) {
columnNames = new String[getColumnCount()];
}
String candidate = columnNames[column];
if (candidate == null) {
columnNames[column] = candidate = titleFormat.format(minX + column);
}
return candidate;
}
/**
* Returns a column index given its name. If no column having this name is found,
* returns -1.
*
* @param name The column name.
* @return The index of the colum of the given name, or -1 if none.
*/
@Override
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.
*
* @param column The index for which to get the sample value type.
* @return The type of sample values at the given index.
*/
@Override
public Class<? extends Number> 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 added with
* {@link #minX} and {@link #minY}.
*/
private 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.
*
* @param y The row index.
* @param x The column index.
* @return The sample value at the requested cell.
*/
@Override
public Number getValueAt(int y, int x) {
final Raster raster = getRasterAt(y += minY, x += minX);
if (raster == null) {
return null;
}
switch (dataType) {
default: return Integer.valueOf(raster.getSample (x, y, band));
case DataBuffer.TYPE_FLOAT: return Float .valueOf(raster.getSampleFloat (x, y, band));
case DataBuffer.TYPE_DOUBLE: return Double .valueOf(raster.getSampleDouble(x, y, band));
}
}
/**
* Returns the color at the specified row and column.
*
* @param y The row index.
* @param x The column index.
* @return The color for the requested cell.
*/
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 JScrollPane#setRowHeaderView
* scroll pane's row header} for an image table.
*
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @see JScrollPane#setRowHeader
*
* @since 2.2
* @module
*/
final 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}.
*/
@Override
public int getRowCount() {
return ImageTableModel.this.getRowCount();
}
/**
* Returns the number of columns in the model, which is 1.
*/
@Override
public int getColumnCount() {
return 1;
}
/**
* Returns the type of row headers, which is {@code String.class}.
*
* @param column The column for which to get the type.
* @return The type for the requested column.
*/
@Override
public Class<String> getColumnClass(final int column) {
return String.class;
}
/**
* Returns the row name for the given index, regardless of the column.
*
* @param row The row of the cell for which to get a value.
* @param column The column of the cell for which to get a value.
* @return The value for the requested cell.
*/
@Override
public String 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.
*
* @param event The change that occurred in the image table model.
*/
@Override
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));
}
}
}
}