/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2007-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.coverage.sql;
import java.nio.file.NoSuchFileException;
import java.util.Map;
import java.util.Set;
import java.util.Arrays;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Collection;
import java.util.Locale;
import java.util.Date;
import java.util.TimeZone;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.rmi.RemoteException;
import java.util.concurrent.CancellationException;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.StringSelection;
import javax.swing.JTable;
import javax.swing.table.TableModel;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.event.TableModelEvent;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoManager;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CannotRedoException;
import org.opengis.coverage.Coverage;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.util.DateRange;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.internal.Threads;
import org.geotoolkit.image.palette.IIOListeners;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.resources.Vocabulary;
import org.geotoolkit.coverage.io.CoverageStoreException;
import static org.apache.sis.util.collection.Containers.hashMapCapacity;
/**
* A <cite>Swing</cite> {@linkplain TableModel Table Model} listing coverages entries. An instance
* of {@code CoverageTableModel} typically contains the coverages available in a particular layer,
* but this is not required.
* <p>
* This class also provides support for <cite>undo</cite>/<cite>redo</cite> operations,
* and can render the table cells in different colors according whatever the coverage file exists
* (the name of missing files are written in red) and if the coverage has been previously visited.
* <p>
* The code below shows how to use this table model in a <cite>Swing</cite> application:
*
* {@preformat java
* Layer layer = coverageDatabase.getLayer("MyLayer");
* CoverageTableModel model = new CoverageTableModel(layer.getCoverageReferences(null), null);
* JTable table = new JTable(model);
*
* // Enable cell coloring (optional).
* TableCellRenderer renderer = new CoverageTableModel.CellRenderer();
* view.setDefaultRenderer(String.class, renderer);
* view.setDefaultRenderer(Date.class, renderer);
*
* // Enable undo/redo manager (optional).
* UndoManager undoManager = new UndoManager();
* model.addUndoableEditListener(undoManager);
*
* // Show the table in a frame.
* JFrame frame = new JFrame(layer.getName());
* frame.add(table);
* frame.pack();
* frame.setVisible(true);
* }
*
* If a undo manager has been setup, the application can invoke the {@link UndoManager#undo()}
* and {@link UndoManager#redo()} methods.
*
* {@section Multi-threading}
* Like most <cite>Swing</cite> class, this class shall not be assumed thread-safe. This class
* is designed for usage from the <cite>event dispatcher</cite> thread, unless otherwise specified.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.11
*
* @see org.geotoolkit.gui.swing.coverage.CoverageList
*
* @since 3.11 (derived from Seagis)
* @module
*/
public class CoverageTableModel extends AbstractTableModel {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 6723633134014245147L;
/**
* {@code true} for displaying most recent records first.
*/
private static final boolean REVERSE_ORDER = true;
/** Column number of file names. */ private static final int NAME = 0;
/** Column number of file dates. */ private static final int DATE = 1;
/** Column number of duration. */ private static final int DURATION = 2;
/**
* Column types. The index shall be the {@link #NAME}, {@link #DATE} or
* {@link #DURATION} constants.
*/
private static final Class<?>[] CLASSES = new Class<?>[3];
static {
CLASSES[NAME] = String.class;
CLASSES[DATE] = String.class; // We format this entry ourself.
CLASSES[DURATION] = String.class;
};
/**
* Column titles. The index shall be the {@link #NAME}, {@link #DATE} or
* {@link #DURATION} constants.
*/
private final String[] titles;
/**
* The list of entries shown by this table model. The length of this array is the
* number of rows in the table model. The elements in this array will be replaced
* by {@code CoverageProxy} instances when first visited, on a element-by-element
* basis.
*/
private GridCoverageReference[] entries;
/**
* The locale to use for formatting the cell content.
* Locale category is {@code Locale.Category.DISPLAY}.
*/
private final Locale locale;
/**
* The formatter to use for dates.
*/
private final DateFormat dateFormat;
/**
* The formatter to use for duration.
*/
private final DateFormat timeFormat;
/**
* The formatter to use for numbers.
*/
private final NumberFormat numberFormat;
/**
* Temporary object for cell formatting.
*/
private transient FieldPosition fieldPosition;
/**
* Temporary buffer for cell formatting.
*/
private transient StringBuffer buffer;
/**
* The word "day" in the user locale.
*/
private final String DAY;
/**
* The word "days" in the user locale.
*/
private final String DAYS;
/**
* Creates a new, initially empty, table model. The initial timezone for the dates column
* is the {@linkplain TimeZone#getDefault() local timezone}, but this can be changed by
* a call to {@link #setTimeZone(TimeZone)} after construction.
*
* @param locale The locale for column titles and cell formatting, or
* {@code null} for the {@linkplain Locale#getDefault() default locale}.
*/
public CoverageTableModel(Locale locale) {
Locale fmtLoc = locale;
if (locale == null) {
locale = Locale.getDefault(Locale.Category.DISPLAY);
fmtLoc = Locale.getDefault(Locale.Category.FORMAT);
}
this.locale = locale;
entries = new GridCoverageReference[0];
numberFormat = NumberFormat.getNumberInstance(fmtLoc);
dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, fmtLoc);
timeFormat = new SimpleDateFormat("HH:mm", fmtLoc);
timeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
final Vocabulary resources = Vocabulary.getResources(locale);
DAY = resources.getString(Vocabulary.Keys.Day);
DAYS = resources.getString(Vocabulary.Keys.Days);
titles = new String[CLASSES.length];
titles[NAME] = resources.getString(Vocabulary.Keys.Name);
titles[DATE] = resources.getString(Vocabulary.Keys.EndTime);
titles[DURATION] = resources.getString(Vocabulary.Keys.Duration);
}
/**
* Creates a new table model initialized to the content of the given collection. The initial
* timezone for the date column is the {@linkplain TimeZone#getDefault() local timezone},
* but this can be changed by a call to {@link #setTimeZone(TimeZone)} after construction.
*
* @param refs References to the coverages to put in the list, or {@code null} if none.
* @param locale The locale for column titles and cell formatting, or
* {@code null} for the {@linkplain Locale#getDefault() default locale}.
*/
public CoverageTableModel(final Collection<? extends GridCoverageReference> refs, final Locale locale) {
this(locale);
if (refs != null) {
entries = refs.toArray(new GridCoverageReference[refs.size()]);
rewrapEntries();
if (REVERSE_ORDER) {
ArraysExt.reverse(entries);
}
}
}
/**
* Creates a new table initialized to the content of the given model.
* This is a copy constructor, except for the listener lists which are not copied.
*
* @param table The table model to copy.
*/
public CoverageTableModel(final CoverageTableModel table) {
titles = table.titles; // No need to clone this one.
DAY = table.DAY;
DAYS = table.DAYS;
locale = table.locale;
numberFormat = (NumberFormat) table.numberFormat.clone();
dateFormat = (DateFormat) table. dateFormat.clone();
timeFormat = (DateFormat) table. timeFormat.clone();
entries = table. entries.clone();
rewrapEntries();
}
/**
* If there is any instance of {@link CoverageProxy} from a foreigner {@code CoverageTableModel}
* in the given array, replace them by new instance for this {@code CoverageTableModel}. This
* method is used by constructors only.
*/
private void rewrapEntries() {
final GridCoverageReference[] entries = this.entries;
for (int i=0; i<entries.length; i++) {
if (entries[i] instanceof CoverageProxy) {
final CoverageProxy oldProxy = (CoverageProxy) entries[i];
final CoverageProxy newProxy = new CoverageProxy(unwrap(oldProxy.reference));
newProxy.flags = oldProxy.flags;
entries[i] = newProxy;
}
}
}
/**
* Sets the content of this table model to the given collection of coverage references.
* Any coverage references previously listed will be removed from this table model.
*
* @param references The new collection of coverage references.
*/
public void setCoverageReferences(final Collection<? extends GridCoverageReference> references) {
final GridCoverageReference[] newEntries = references.toArray(new GridCoverageReference[references.size()]);
if (REVERSE_ORDER) {
ArraysExt.reverse(newEntries);
}
final GridCoverageReference[] oldEntries = entries;
/*
* Get the list of CoverageProxy instances that existed before this method call.
* We will try to recycle existing instances in order to preserve the information
* about whatever the file exists, etc.
*/
Map<GridCoverageReference,CoverageProxy> proxies = null;
for (GridCoverageReference entry : oldEntries) {
if (entry instanceof CoverageProxy) {
if (proxies == null) {
proxies = new HashMap<>();
}
final CoverageProxy proxy = (CoverageProxy) entry;
final CoverageProxy old = proxies.put(proxy.reference, proxy);
assert old == null || old == proxy : proxy;
}
}
if (proxies != null) {
for (int i=0; i<newEntries.length; i++) {
final CoverageProxy proxy = proxies.get(newEntries[i]);
if (proxy != null) {
newEntries[i] = proxy;
}
}
}
entries = newEntries;
fireTableDataChanged();
commitEdit(oldEntries, newEntries, Vocabulary.Keys.Define);
}
/**
* If the given {@code entry} is of kind {@link CoverageProxy},
* returns the original reference.
*/
private static GridCoverageReference unwrap(GridCoverageReference entry) {
while (entry instanceof CoverageProxy) {
entry = ((CoverageProxy) entry).reference;
}
return entry;
}
/**
* Returns all coverage references listed by this table model.
* <p>
* <b>Note:</b> If a call to {@link GridCoverageReference#read GridCoverageReference.read}
* is planed, consider using the reference returned by {@link #getCoverageReferenceAt(int)}
* instead.
*
* @return The coverage references listed by this table model, or an empty array if none
* (never {@code null}).
*/
public GridCoverageReference[] getCoverageReferences() {
final GridCoverageReference[] entries = this.entries;
final GridCoverageReference[] out = new GridCoverageReference[entries.length];
for (int i=0; i<out.length; i++) {
out[i] = unwrap(entries[i]);
}
return out;
}
/**
* Returns the coverage reference at the given row index. This method returns a special
* reference which will take trace of whatever a call to {@link GridCoverageReference#read
* GridCoverageReference.read} succeed or not, and will apply a color on the corresponding
* table cell accordingly.
*
* @param row The index of the desired coverage reference.
* @return The coverage reference at the given row.
*/
public GridCoverageReference getCoverageReferenceAt(final int row) {
GridCoverageReference entry = entries[row];
if (!(entry instanceof CoverageProxy)) {
entries[row] = entry = new CoverageProxy(entry);
}
return entry;
}
/**
* Returns the name of all coverages in this table model. The names are computed by
* {@link #getCoverageName(GridCoverageReference)} and are usually unique for a given
* layer.
*
* @return The coverage names listed by this table model, or an empty array if none
* (never {@code null}).
*/
public String[] getCoverageNames() {
final GridCoverageReference[] entries = this.entries;
final String[] names = new String[entries.length];
for (int i=0; i<names.length; i++) {
names[i] = getCoverageName(entries[i]);
}
return names;
}
/**
* Returns the name of coverages at the given row indices in this table model. The names
* are computed by {@link #getCoverageName(GridCoverageReference)} and are usually unique
* for a given layer.
*
* @param rows The rows for which to get the coverage names.
* @return The coverage names at the given indices.
* The length of this array is equals to {@code rows.length}.
*/
public String[] getCoverageNames(final int... rows) {
final GridCoverageReference[] entries = this.entries;
final String[] names = new String[rows.length];
for (int i=0; i<names.length; i++) {
names[i] = getCoverageName(entries[rows[i]]);
}
return names;
}
/**
* Returns the row indices of coverage references having the given name. The given names shall
* be built in the same way than what the {@link #getCoverageName(GridCoverageReference)}
* method do. This method is the converse of {@link #getCoverageNames(int[])}.
* <p>
* If no image is found for a given name, the indices of the corresponding element in the
* returned array is set to -1.
*
* @param names The coverage reference names.
* @return Row indices of named coverages. The length of this array is equals to
* {@code names.length}. The array may contains -1 values for image not found.
*/
public int[] indexOf(final String... names) {
/*
* Get the indices (in the names array) of each name. A name
* can have more than one index if it appears many time.
*/
final Map<String,int[]> map = new HashMap<>(hashMapCapacity(names.length));
for (int i=0; i<names.length; i++) {
int[] index = map.put(names[i], new int[]{i});
if (index != null) {
// In case the same name is requested more than once.
final int length = index.length;
index = ArraysExt.resize(index, length+1);
index[length] = i;
map.put(names[i], index);
}
}
/*
* Scan the entries in this table. For each entry having a name matching one of the
* requested names, set all corresponding elements in the 'rows' array to the row
* indice.
*/
final int[] rows = new int[names.length];
Arrays.fill(rows, -1);
final GridCoverageReference[] entries = this.entries;
for (int i=0; i<entries.length; i++) {
final String name = getCoverageName(entries[i]);
int[] index = map.remove(name);
if (index != null) {
for (int j=0; j<index.length; j++) {
rows[index[j]] = i;
}
// If the same name has been requested more than once, then the next occurrences
// of this name will leave the index of the previous occurrence unchanged, and
// update the indices of the other occurrences.
if (index.length > 1) {
map.put(name, ArraysExt.remove(index, 0, 1));
}
}
}
return rows;
}
/**
* Removes one or many rows from this table.
*
* @param rows The rows to remove.
*/
public void remove(final int... rows) {
final Set<GridCoverageReference> toRemoveSet;
toRemoveSet = new HashSet<>(hashMapCapacity(rows.length));
for (int i=0; i<rows.length; i++) {
toRemoveSet.add(unwrap(entries[rows[i]]));
}
remove(toRemoveSet);
}
/**
* Removes one or many coverage references from this table. If a given coverage
* reference is not found in this table, then that reference is ignored.
*
* @param toRemove The coverage references to remove.
*/
public void remove(final GridCoverageReference... toRemove) {
final Set<GridCoverageReference> toRemoveSet;
toRemoveSet = new HashSet<>(hashMapCapacity(toRemove.length));
for (int i=0; i<toRemove.length; i++) {
toRemoveSet.add(unwrap(toRemove[i]));
}
remove(toRemoveSet);
}
/**
* Removes one or many coverage references from this table. If a given coverage
* reference is not found in this table, then that reference is ignored.
*
* @param toRemove The coverage references to remove.
*/
private void remove(final Set<GridCoverageReference> toRemove) {
final GridCoverageReference[] oldEntries = entries;
GridCoverageReference[] entries = oldEntries;
int entriesLength = entries.length;
int upper = entriesLength;
for (int i=upper; --i>=-1;) {
if (i<0 || !toRemove.contains(unwrap(entries[i]))) {
final int lower = i+1;
if (upper != lower) {
if (entries == oldEntries) {
// Create a copy, so we don't modify the original array.
entries = ArraysExt.remove(entries, lower, upper-lower);
} else {
// Work directly on the array only if we known that it is a copy.
System.arraycopy(entries, upper, entries, lower, entriesLength-upper);
}
entriesLength -= upper - lower;
fireTableRowsDeleted(lower, upper-1);
}
upper = i;
}
}
this.entries = ArraysExt.resize(entries, entriesLength);
commitEdit(oldEntries, this.entries, Vocabulary.Keys.Delete);
}
/**
* Returns a transferable which can be set to the clipboard. A <cite>Copy</cite>
* action in a <cite>Swing</cite> application could be implemented as below:
*
* {@preformat java
* Transferable tr = model.copy(rows);
* Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
* cb.setContents(tr, owner);
* }
*
* @param rows The indices of the rows to copy in the transferable object.
* @return A copy of the specified rows as a transferable object.
*/
public Transferable copy(final int[] rows) {
if (fieldPosition == null) {
fieldPosition = new FieldPosition(0);
}
/*
* Don't use the cell buffer.
*
* Note: Use the '\n' line separator, not System.getProperty("line.separator", "\n"),
* because the later produces strange result when pasted in Excel: an empty line
* is inserted between every rows.
*/
final StringBuffer buffer = new StringBuffer(256);
final short[] keys = {
Vocabulary.Keys.Name,
Vocabulary.Keys.StartTime,
Vocabulary.Keys.EndTime
};
final Vocabulary resources = Vocabulary.getResources(locale);
for (int i=0; i<keys.length;) {
buffer.append(resources.getString(keys[i++]));
buffer.append((i != keys.length) ? '\t' : '\n');
}
for (int i=0; i<rows.length; i++) {
Date date;
final GridCoverageReference entry = unwrap(entries[rows[i]]);
final DateRange timeRange = entry.getTimeRange();
buffer.append(getCoverageName(entry)).append('\t');
if ((date = timeRange.getMinValue()) != null) {
dateFormat.format(date, buffer, fieldPosition);
}
buffer.append('\t');
if ((date = timeRange.getMaxValue()) != null) {
dateFormat.format(date, buffer, fieldPosition);
}
}
return new StringSelection(buffer.append('\n').toString());
}
/**
* Returns the number of rows in the model.
*/
@Override
public int getRowCount() {
return entries.length;
}
/**
* Returns the number of columns in the model.
*/
@Override
public int getColumnCount() {
return titles.length;
}
/**
* Returns the column name at the given index.
*/
@Override
public String getColumnName(final int column) {
return titles[column];
}
/**
* Returns the type of cell values in the column at the given index.
*
* @param column The column index.
*/
@Override
public Class<?> getColumnClass(final int column) {
return CLASSES[column];
}
/**
* Returns the cell value at the given row and column.
*
* @param row The 0-based row index.
* @param column The 0-based column index.
* @return The cell value at the given location.
*/
@Override
public Object getValueAt(final int row, final int column) {
GridCoverageReference entry = entries[row];
if (!(entry instanceof CoverageProxy)) {
entries[row] = entry = new CoverageProxy(entry);
}
switch (column) {
case NAME: return getCoverageName(entry);
case DATE: return format(entry.getTimeRange().getMaxValue());
case DURATION: {
final DateRange range = entry.getTimeRange();
final Date time = range.getMaxValue();
final Date start = range.getMinValue();
if (time != null && start != null) {
final long millis = time.getTime() - start.getTime();
final long days = millis / (24L*60*60*1000);
time.setTime(millis);
final StringBuffer buffer = getBuffer();
if (days != 0) {
numberFormat.format(days, buffer, fieldPosition)
.append(' ').append((days > 1) ? DAYS : DAY).append(' ');
}
return timeFormat.format(time, buffer, fieldPosition).toString();
}
break;
}
}
return null;
}
/**
* Returns the coverage name to write in the table cell. The default implementation returns
* {@link GridCoverageReference#getName()}. Subclasses can override this method in order to
* build the name differently.
*
* @param entry The coverage reference for which to get the name.
* @return The coverage name to write in the table cell.
*/
protected String getCoverageName(final GridCoverageReference entry) {
return entry.getName();
}
/**
* Returns the string buffer to use for formatting purpose.
*/
private StringBuffer getBuffer() {
if (buffer == null) {
buffer = new StringBuffer();
}
if (fieldPosition == null) {
fieldPosition = new FieldPosition(0);
}
buffer.setLength(0);
return buffer;
}
/**
* Formats the given name.
*/
private String format(final Date date) {
if (date == null) {
return null;
}
return dateFormat.format(date, getBuffer(), fieldPosition).toString();
}
/**
* Returns the current timezone used for formatting dates.
*
* @return The timezone used for formatting dates.
*/
public TimeZone getTimeZone() {
return dateFormat.getTimeZone();
}
/**
* Sets the timezone to use for formatting dates.
*
* @param timezone The new timezone to use.
*/
public void setTimeZone(final TimeZone timezone) {
dateFormat.setTimeZone(timezone);
if (entries.length != 0) {
fireTableChanged(new TableModelEvent(this, 0, entries.length-1, DATE));
}
}
/**
* Adds a new object to inform every time a undoable action has been performed.
*
* @param listener The listener to add.
*/
public void addUndoableEditListener(final UndoableEditListener listener) {
listenerList.add(UndoableEditListener.class, listener);
}
/**
* Removes an object from the list of listeners to inform about undoable actions.
*
* @param listener The listener to remove.
*/
public void removeUndoableEditListener(final UndoableEditListener listener) {
listenerList.remove(UndoableEditListener.class, listener);
}
/**
* Invoked when an undoable action has been performed.
*/
private void commitEdit(final GridCoverageReference[] oldEntries,
final GridCoverageReference[] newEntries,
final short key)
{
final String name = Vocabulary.getResources(locale).getString(key).toLowerCase();
@SuppressWarnings("serial")
final class EditEvent extends AbstractUndoableEdit {
/** Undo the edit. */
@Override public void undo() throws CannotUndoException {
super.undo();
entries = oldEntries;
fireTableDataChanged();
}
/** Redo the edit. */
@Override public void redo() throws CannotRedoException {
super.redo();
entries = newEntries;
fireTableDataChanged();
}
/** Returns a name describing the edit. */
@Override public String getPresentationName() {
return name;
}
}
if (oldEntries != newEntries) {
final Object[] listeners = listenerList.getListenerList();
if (listeners.length != 0) {
UndoableEditEvent event = null;
for (int i=listeners.length; (i-=2) >= 0;) {
if (listeners[i] == UndoableEditListener.class) {
if (event == null) {
event = new UndoableEditEvent(this, new EditEvent());
}
((UndoableEditListener) listeners[i+1]).undoableEditHappened(event);
}
}
}
}
}
/**
* Invoked when the row for the given reference has been updated.
*/
final void fireTableRowsUpdated(GridCoverageReference entry) {
entry = unwrap(entry);
final GridCoverageReference[] entries = this.entries;
for (int i=entries.length; --i>=0;) {
if (entry.equals(unwrap(entries[i]))) {
fireTableRowsUpdated(i, i);
}
}
}
/**
* A {@link GridCoverageReference} which intercept the read methods in order to track
* whatever the reading succeed or failed.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.16
*
* @since 3.11 (derived from Seagis)
* @module
*/
private final class CoverageProxy extends GridCoverageDecorator {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 8398851451224196337L;
/** Bit-flag for an entry visited. */ public static final byte VIEWED = 1;
/** Bit-flag for a missing file. */ public static final byte MISSING = 2;
/** Bit-flag for a corrupted file. */ public static final byte CORRUPTED = 4;
/** Bit-flag for a RMI failure. */ public static final byte RMI_FAILURE = 8;
/**
* The status of this entry as a bit mask.
*/
byte flags;
/**
* Creates a new wrapper for the given entry.
*/
CoverageProxy(final GridCoverageReference entry) {
super(entry);
FileChecker.add(this);
}
/**
* Reads the given image. If the read operation succeed, then the {@link #VIEWED}
* flag is set. If the read operation fails, then the {@link #CORRUPTED} flag is set.
*/
@Override
public Coverage getCoverage(final IIOListeners listeners) throws IOException {
try {
final Coverage image = reference.getCoverage(listeners);
setFlag((byte) (MISSING|CORRUPTED|RMI_FAILURE), false);
setFlag(VIEWED, image != null);
return image;
} catch (RemoteException exception) {
setFlag(RMI_FAILURE, true);
throw exception;
} catch (FileNotFoundException | NoSuchFileException exception) {
setFlag(MISSING, true);
throw exception;
} catch (IOException exception) {
setFlag(CORRUPTED, true);
throw exception;
}
}
/**
* Reads the given image. If the read operation succeed, then the {@link #VIEWED}
* flag is set. If the read operation fails, then the {@link #CORRUPTED} flag is set.
*/
@Override
public GridCoverage2D read(final CoverageEnvelope envelope, final IIOListeners listeners)
throws CoverageStoreException, CancellationException
{
try {
final GridCoverage2D image = reference.read(envelope, listeners);
setFlag((byte) (MISSING|CORRUPTED|RMI_FAILURE), false);
setFlag(VIEWED, image != null);
return image;
} catch (CoverageStoreException exception) {
final Throwable cause = exception.getCause();
if (cause instanceof RemoteException) {
setFlag(RMI_FAILURE, true);
} else if (cause instanceof FileNotFoundException || cause instanceof NoSuchFileException) {
setFlag(MISSING, true);
} else if (cause instanceof IOException) {
setFlag(CORRUPTED, true);
}
throw exception;
}
}
/**
* Sets or clear the given flags. If this method changed the flag state,
* then {@link #fireTableRowsUpdated} is invoked.
*
* {@section Multi-threading}
* This method may be invoked from any thread: either the Swing thread or the background
* thread created by {@link FileChecker}. Consequently this method must be thread-safe.
*/
final synchronized void setFlag(byte f, final boolean set) {
if (set) f |= flags;
else f = (byte) (flags & ~f);
if (flags != f) {
flags = f;
if (EventQueue.isDispatchThread()) {
fireTableRowsUpdated(reference);
} else EventQueue.invokeLater(new Runnable() {
@Override public void run() {
fireTableRowsUpdated(reference);
}
});
}
}
}
/**
* Background task checking for file existence.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.11
*
* @since 3.11 (derived from Seagis)
* @module
*/
private static final class FileChecker implements Runnable {
/**
* The task currently under execution.
*/
private static FileChecker running;
/**
* List of coverage references to check for existence.
*/
private final LinkedList<CoverageProxy> list = new LinkedList<>();
/**
* Only {@link FileChecker} is allowed to instantiate this class.
*/
private FileChecker() {
}
/**
* Adds an entry to the list of entries pending validity check.
*/
public static synchronized void add(final CoverageProxy entry) {
if (entry != null) {
if (running == null) {
running = new FileChecker();
Threads.executeWork(running);
}
running.list.add(entry);
}
}
/**
* Returns the next image to check from the given list, or {@code null}
* if the list is empty. In the later case, the thread will terminate.
*/
private static synchronized CoverageProxy next(final LinkedList<CoverageProxy> list) {
if (list.isEmpty()) {
running = null;
return null;
}
return list.removeFirst();
}
/**
* Checks if the file for the given entry exists. If a file is not found, then the
* {@link CoverageProxy#MISSING} flag for the corresponding entry will be set.
*/
@Override
public void run() {
CoverageProxy entry;
while ((entry = next(list)) != null) {
try {
final File file = entry.getFile(File.class);
if (file.isAbsolute()) {
entry.setFlag(CoverageProxy.MISSING, !file.isFile());
}
} catch (IOException e) {
entry.setFlag(CoverageProxy.CORRUPTED, true);
Logging.recoverableException(null, GridCoverageReference.class, "getPath", e);
}
}
}
}
/**
* A {@linkplain TableCellRenderer Table Cell Renderer} for coloring the cells in a table
* using the {@linkplain CoverageTableModel Coverage Table Model}. Normal rows are rendered
* with the default color (usually black). The rows corresponding to coverages which have been
* {@linkplain GridCoverageReference#read read} are written in blue, and the rows corresponding
* to files not found are written in red.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.11
*
* @since 3.11 (derived from Seagis)
* @module
*/
@SuppressWarnings("serial")
public static class CellRenderer extends DefaultTableCellRenderer {
/**
* The default foreground color.
*/
private Color foreground;
/**
* The default background color.
*/
private Color background;
/**
* Creates a new {@code CellRenderer}.
*/
public CellRenderer() {
super();
foreground = super.getForeground();
background = super.getBackground();
}
/**
* Sets the default foreground color.
*
* @param foreground The new foreground color.
*/
@Override
public void setForeground(final Color foreground) {
super.setForeground(this.foreground = foreground);
}
/**
* Sets the default background color.
*
* @param background The new background color.
*/
@Override
public void setBackground(final Color background) {
super.setBackground(this.background = background);
}
/**
* Returns the AWT component to use for painting the cell at the given location.
*/
@Override
public Component getTableCellRendererComponent(final JTable table, Object value,
final boolean isSelected, final boolean hasFocus, final int row, final int column)
{
Color foreground = this.foreground;
Color background = this.background;
if (row >= 0) {
final TableModel model = table.getModel();
if (model instanceof CoverageTableModel) {
final GridCoverageReference entry;
final CoverageTableModel imageTable = (CoverageTableModel) model;
if (value instanceof Date) {
value = imageTable.format((Date) value);
}
entry = imageTable.entries[row];
if (entry instanceof CoverageProxy) {
final byte flags = ((CoverageProxy) entry).flags;
if ((flags & CoverageProxy.VIEWED ) != 0) {foreground=Color.BLUE;}
if ((flags & CoverageProxy.MISSING ) != 0) {foreground=Color.RED;}
if ((flags & CoverageProxy.CORRUPTED ) != 0) {foreground=Color.WHITE; background=Color.RED;}
if ((flags & CoverageProxy.RMI_FAILURE) != 0) {foreground=Color.BLACK; background=Color.YELLOW;}
}
}
}
super.setBackground(background);
super.setForeground(foreground);
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
}
}