/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-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.util.List; import java.util.ArrayList; import java.awt.EventQueue; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.io.File; import java.io.IOException; import java.util.Locale; import javax.swing.table.TableModel; import javax.swing.table.DefaultTableModel; import javax.swing.table.AbstractTableModel; import javax.imageio.spi.ImageReaderSpi; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.Classes; import org.geotoolkit.process.ProgressController; import org.geotoolkit.image.io.mosaic.Tile; import org.geotoolkit.image.io.mosaic.TileManager; import org.geotoolkit.image.io.mosaic.TileManagerFactory; import org.geotoolkit.gui.swing.ListTableModel; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.internal.swing.SwingUtilities; /** * A table model for a list of {@linkplain Tile tiles}. The default implementation provides one * row for each tile {@linkplain #add(Collection) added} to this model and one column for each * tile property listed below. This table model does not allows cell edition but allows row * insertion and removal, assuming that the {@linkplain #elements list of tiles} is modifiable. * The default columns are: * <p> * <ul> * <li>The {@linkplain Tile#getInputName input name}</li> * <li>The {@linkplain Tile#getImageIndex image index}</li> * <li>The {@linkplain Tile#getSize tile size} (two columns for width and height)</li> * <li>The {@linkplain Tile#getLocation tile location} (two columns for <var>x</var> and * <var>y</var>)</li> * <li>The {@linkplain Tile#getSubsampling subsampling} (two columns for <var>x</var> and * <var>y</var> axis)</li> * </ul> * * {@section External list of tiles} * A list of tiles is given to the constructor and retained by direct reference - the list is * <strong>not</strong> cloned, because it may contains millions of tiles, sometime through a * custom {@link List} implementation fetching the information on-the-fly from a database. * Consequently if the content of the list is modified externally, then a {@code fireXXX} * method (inherited from {@link AbstractTableModel} must be invoked explicitly. Note that * the {@code fireXXX} methods don't need to be invoked if the list is modified through the * methods provided in this class. * * {@section Multi-threading} * Unless otherwise specified, every methods in this class must be invoked from the Swing * thread. The main exceptions are the methods listed below, which should actually be invoked * from a background thread rather than the Swing thread: * <p> * <ul> * <li>{@link #add(ImageReaderSpi, File[], ProgressController)}</li> * <li>{@link #getTileManager()}</li> * </ul> * * {@section Serialization} * This model is serialiable if the underlying list of tiles is serializable. * * @author Martin Desruisseaux (Geomatys) * @version 3.00 * * @see MosaicChooser * * @since 3.00 * @module */ public class MosaicTableModel extends ListTableModel<Tile> { /** * Serial number for compatibility with different versions. */ private static final long serialVersionUID = 1863722624530354663L; /** * Dummy dimension to be used when we have determined that no dimension are available. */ private static final Dimension NO_DIMENSION = new Dimension(); /** * Dummy location to be used when we have determined that no location are available. */ private static final Point NO_LOCATION = new Point(); /** * An optional locale for column headers and error messages, or {@code null} * for the default. */ Locale locale; /** * The last tile queried by {@link #getValueAt}. If non-null, then the values of * {@link #size}, {@link #location} and {@link #subsampling} are about that tile. * Those values are saved for avoiding querying the same object many time while * rendering the same row. */ private transient Tile last; /** * The value of {@link Tile#getSize} for the {@linkplain #last} tile. */ private transient Dimension size; /** * The value of {@link Tile#getLocation} for the {@linkplain #last} tile. */ private transient Point location; /** * The value of {@link Tile#getSubsampling} for the {@linkplain #last} tile. */ private transient Dimension subsampling; /** * Creates a new table model backed by an {@link ArrayList} of tiles. */ public MosaicTableModel() { super(Tile.class); } /** * Creates a new table model for the given list of tiles. The given list is retained * by direct reference - it is not cloned. Consequently if the content of this list * is modified externaly, then one of the {@code fireXXX} method inherited from * {@link AbstractTableModel} must be invoked explicitly. * * @param tiles The list of tiles to display in a table. */ public MosaicTableModel(final List<Tile> tiles) { super(Tile.class, tiles); } /** * Returns the number of columns in the table. * * @return The number of columns. */ @Override public int getColumnCount() { return 8; } /** * Returns the name of the given column. * * @param column The column. * @return The column name. */ @Override public String getColumnName(final int column) { final short key; switch (column) { case 0: key = Vocabulary.Keys.File; break; case 1: key = Vocabulary.Keys.Index; break; case 2: key = Vocabulary.Keys.Width; break; case 3: key = Vocabulary.Keys.Height; break; case 4: return "x"; case 5: return "y"; case 6: return "sx"; case 7: return "sy"; default: throw new IndexOutOfBoundsException(String.valueOf(column)); } return Vocabulary.getResources(locale).getString(key); } /** * Returns the class of values at the given column. * * @param column The column. * @return The classes of values. */ @Override public Class<?> getColumnClass(final int column) { switch (column) { case 0: return String.class; default: return Integer.class; } } /** * Returns the value at the given index. This method may return {@code null} if the * value at the given cell can not be obtained, for example because of an I/O error. * * @param row The row, which is also the index of the tile. * @param column The column. * @return The value at the given row and column, or {@code null}. */ @Override @SuppressWarnings("fallthrough") public Object getValueAt(final int row, final int column) { final int value; final Tile tile = elements.get(row); if (tile != last) { last = tile; size = null; location = null; subsampling = null; } boolean horizontal = false; switch (column) { case 0: return tile.getInputName(); case 1: value = tile.getImageIndex(); break; case 2: horizontal = true; // Fall through case 3: { if (size == null) try { size = tile.getSize(); } catch (IOException e) { // Can't get the size. For now leave the cell empty. In a future version we // may render the row in a different color to make the error more obvious. size = NO_DIMENSION; } if (size == NO_DIMENSION) return null; value = (horizontal ? size.width : size.height); break; } case 4: horizontal = true; // Fall through case 5: { if (location == null) try { location = tile.getLocation(); } catch (IllegalStateException e) { // Same rational than the catch for IOException above. location = NO_LOCATION; } if (location == NO_LOCATION) return null; value = (horizontal ? location.x : location.y); break; } case 6: horizontal = true; // Fall through case 7: { if (subsampling == null) try { subsampling = tile.getSubsampling(); } catch (IllegalStateException e) { // Same rational than the catch for IOException above. subsampling = NO_DIMENSION; } if (subsampling == NO_DIMENSION) return null; value = (horizontal ? subsampling.width : subsampling.height); break; } default: { throw new IndexOutOfBoundsException(String.valueOf(column)); } } return value; } /** * Adds tiles to be constructed from the given array of files. Every file in the given array * must exist, be a valid image and have a valid <cite>World File</cite>, i.e. a file of the * same name in the same directory with {@code ".tfw"} extension (for TIFF images) or * {@code ".jgw"} extension (for JPEG images). * <p> * This method loads the World Files and fetches the image sizes immediately. The world file * applies to the first image in the file. If the file contains more than one image, then each * additional image is assumed to represent the same data than the first image at a different * resolution. * <p> * This method may be slow and should be invoked in a background thread. After having built * the collection of tiles, this method invokes {@link #add(Collection)} in the Swing thread, * {@linkplain #removeDuplicates removes duplicates} and {@linkplain #sort sorts} the result. * <p> * On success, this method returns {@code null}. If this method failed to constructs some * tiles, then the successful tiles are added as explained above and the unused files are * returned in a new table model. * * @param provider The image reader provider to use for reading image data, or {@code null} * for attempting an automatic detection. * @param files The files from which to creates tiles. * @param progress An optional controller for reporting progresses, or {@code null} if none. * @throws UnsupportedOperationException if the underlying {@linkplain #elements list of tiles} * is not modifiable. * @return {@code null} on success, or a list of files that failed otherwise. */ public TableModel add(final ImageReaderSpi provider, final File[] files, final ProgressController progress) throws UnsupportedOperationException { if (progress != null) { progress.setTask(Vocabulary.formatInternational(Vocabulary.Keys.LoadingHeaders)); progress.started(); } final List<Tile> toAdd = new ArrayList<>(files.length); Class<? extends Exception> lastFailureType = null; DefaultTableModel failures = null; for (int i=0; i<files.length; i++) { final File file = files[i]; final List<Tile> tiles; try { tiles = TileManagerFactory.DEFAULT.listTiles(provider, file.toPath()); } catch (Exception e) { if (progress != null) { /* * Report the failure in the progress windows only if it is a new kind of * failure, otherwise we are at risk of flooding the ProgressWindows and * make Swing unresponsive. Note that the warnings reported here are only for * getting user's attention, since a full table of failures is built anyway. */ final Class<? extends Exception> failureType = e.getClass(); if (!failureType.equals(lastFailureType)) { lastFailureType = failureType; String message = e.getLocalizedMessage(); if (message == null) { message = Classes.getShortClassName(e); } progress.warningOccurred(file.getName(), null, message); } } /* * The error was reported in the ProgressWindow above, if it was non-null. * In addition creates a table of failures to be returned to the user, to * be used or ignored at user's choice. */ if (failures == null) { failures = new DefaultTableModel(); final Vocabulary resources = Vocabulary.getResources(locale); failures.setColumnIdentifiers(new String[] { resources.getString(Vocabulary.Keys.File), resources.getString(Vocabulary.Keys.Error), resources.getString(Vocabulary.Keys.Message) }); } failures.addRow(new String[] { file.getName(), Classes.getShortClassName(e), e.getLocalizedMessage() }); continue; } /* * Adds the tile to the list of tile to be added. */ toAdd.addAll(tiles); if (progress != null) { progress.setProgress(100f * i/files.length); if (progress.isCanceled()) { break; } } } /* * At this point, we have a list of tiles to be added and we got the metadata * that we will need (TFW, image size). Process to the addition in Swing thread, * then remove duplicated and sort. */ if (!toAdd.isEmpty()) { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { int count = elements.size(); add(toAdd); count -= removeDuplicates(); recreate(count); sort(); } }); } if (progress != null) { progress.completed(); } return failures; } /** * Recreates every tiles in the current list which were there before the addition of * new tiles. This is needed in order to get {@link TileManager} to recompute their * affine transforms. Only the {@code n} first tiles will be recreated. */ private void recreate(final int n) { boolean changed = false; for (int i=0; i<n; i++) { Tile tile = elements.get(i); final Rectangle region; try { region = tile.getRegion(); } catch (IOException e) { // Should not happen, but if it does anyway keep the current tile, // which is likely to be painted as an invalid tile by MosaicPanel. Logging.recoverableException(null, MosaicTableModel.class, "add", e); continue; } final AffineTransform tr; try { tr = tile.getGridToCRS(); } catch (IllegalStateException e) { // Should not happen, but if it does anyway it means that the tile // has not yet been processed by TileManagerFactory, in which case // we don't need to recreate it. Logging.recoverableException(null, MosaicTableModel.class, "add", e); continue; } tile = new Tile(tile.getImageReaderSpi(), tile.getInput(), tile.getImageIndex(), region, tr); elements.set(i, tile); changed = true; } if (changed) { fireTableRowsUpdated(0, n-1); } } /** * Returns a tile manager for the current {@linkplain #elements list of tiles}. This method * should return an array of length 1, but a different length may be obtained if it was * not possible to create a single tile manager. * <p> * Consider invoking {@link #removeDuplicates} before this method, since duplicated * tiles are likely to produce erroneous tile manager. Consider also invoking this * {@code getTileManager()} method from a background thread, since it may be slow * (it is safe to invoke this particular method from non-Swing thread). * * @return The tile managers for the current list of tiles. * @throws IOException if an error occurred while reading a tile. */ public TileManager[] getTileManager() throws IOException { final Tile[] elements = getElements(); final TileManager[] managers = TileManagerFactory.DEFAULT.create(elements); /* * TileManagerFactory has computed some properties previously unavailable, * like the tile location computed from the "gridToCRS" affine transform. * Fire a "row updates" event so we can see the updated values in the table. */ EventQueue.invokeLater(new Runnable() { @Override public void run() { final int size = MosaicTableModel.this.elements.size(); if (size != 0) { fireTableRowsUpdated(0, size - 1); } } }); return managers; } }