/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007-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.util.List; import java.util.Arrays; import java.util.Iterator; import java.util.Collection; import java.net.URI; import java.io.File; import java.io.IOException; import java.sql.SQLException; import javax.imageio.ImageReader; import javax.imageio.IIOException; import org.opengis.util.FactoryException; import org.geotoolkit.image.io.XImageIO; import org.geotoolkit.image.io.mosaic.Tile; import org.geotoolkit.image.io.NamedImageStore; import org.geotoolkit.image.io.AggregatedImageStore; import org.geotoolkit.internal.sql.table.SpatialDatabase; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.resources.Errors; /** * An iterator creating {@link NewGridCoverageReference} on-the-fly using different input source. * For the sake of simplicity, this method does not have {@code hasNext()} method. Instead, the * {@code #next()} method returns {@code null} when there is no more element to return. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.16 * * @since 3.12 (derived from Seagis) * @module */ final class NewGridCoverageIterator { /** * The object which contains the listeners. While we are keeping a reference to the * full {@link CoverageDatabase} objects, only the listeners are of interest to this * iterator. */ private final CoverageDatabase listeners; /** * An optional controller to invoke before the listeners, or {@code null} if none. */ private final CoverageDatabaseController controller; /** * The database where new entries will be added. */ private final SpatialDatabase database; /** * An iterator over the inputs to read. * If input are {@link File} or {@link URI}, they shall be relative to current directory. * Inputs may also be {@link Tile} or {@link ImageReader} instances. */ private final Iterator<?> inputToAdd; /** * If the last returned element is actually an aggregation of other files (e.g. a NcML file * which is basically a XML file listing many NetCDF files), the list of aggregated files. * Otherwise {@code null}. * * @since 3.16 */ private List<URI> aggregatedFiles; /** * Creates an iterator for the specified files. * * @param listeners The object which hold the {@link CoverageDatabaseListener}s. While this * argument is of kind {@link CoverageDatabase}, only the listeners are of * interest to this class. * @param controller An optional controller to invoke before the listeners, or {@code null}. * @param database The database where new entries will be added. This is mandatory. * @param inputToAdd The files to read. * @throws IOException if an I/O operation was required and failed. */ NewGridCoverageIterator(final CoverageDatabase listeners, final CoverageDatabaseController controller, final SpatialDatabase database, final Collection<?> inputToAdd) throws SQLException, IOException, FactoryException, DatabaseVetoException { this.listeners = listeners; this.controller = controller; this.database = database; this.inputToAdd = inputToAdd.iterator(); } /** * Creates an entry for the given input. If this method detects that there is many images * in the file, then {@link CoverageDatabaseController#filterImages} is invoked. Finally, * the {@link NewGridCoverageReference} constructor will fetch the image metadata. * * @param input The input. * @return The entry. * @throws IOException if an I/O operation was required and failed. */ private NewGridCoverageReference createEntry(Object input) throws SQLException, IOException, FactoryException, DatabaseVetoException { aggregatedFiles = null; if (input instanceof NewGridCoverageReference) { return (NewGridCoverageReference) input; } if (input instanceof Tile) { return new NewGridCoverageReference(database, (Tile) input); } final ImageReader reader; final boolean disposeReader; if (input instanceof ImageReader) { reader = (ImageReader) input; input = reader.getInput(); disposeReader = false; } else { /* * If there is a controller, then 'seekForwardOnly' must be set to 'false' in order * to allow the call to ImageReader.getNumImages(true) inside the controller block. */ reader = XImageIO.getReaderBySuffix(input, controller == null, false); disposeReader = true; } /* * If there is many images, get the list of them. If the file is some format having * named images (e.g. NetCDF files where images are actually NetCDF variables), then * this is the list of those variables. Otherwise we generate a list with "Image 1", * "Image 2", etc. items. After the list has been created, ask the controller to * choose some images in that list. The selection may happen in a Swing GUI. */ int imageIndex = 0; if (controller != null) { final int numImages = reader.getNumImages(true); if (numImages > 1) { final boolean multiSelectionAllowed = (reader instanceof NamedImageStore); final List<String> variables; if (multiSelectionAllowed) { variables = ((NamedImageStore) reader).getImageNames(); } else { final String[] names = new String[numImages]; final Vocabulary resources = Vocabulary.getResources(listeners.getLocale()); for (int i=0; i<names.length; i++) { names[i] = resources.getString(Vocabulary.Keys.Image_1, i+1); } variables = Arrays.asList(names); } final Collection<String> selected = controller.filterImages(variables, multiSelectionAllowed); if (selected != null) { /* * At this point, the controller selected some images in the proposed list. * Get the image index of each selected item. If the image reader supports * named bands, each selected item will be a band of the only image to be * read. For example in a NetCDF file, the "U" and "V" variable may be two * bands of the same image. For any other kind of image reader, the selection * can contain only one image. */ int numSelected = 0; final int[] index = new int[selected.size()]; for (final String variable : selected) { if ((index[numSelected++] = variables.indexOf(variable)) < 0) { throw new IIOException(error(Errors.Keys.NoSuchElementName_1, variable)); } } if (numSelected != 0) { if (multiSelectionAllowed) { final String[] names = new String[numSelected]; for (int i=0; i<names.length; i++) { names[i] = variables.get(index[i]); } final NamedImageStore store = (NamedImageStore) reader; store.setImageNames(names[0]); store.setBandNames(0, names); // Leave the imageIndex to 0: it may be used for images at different dates. } else if (numSelected != 1) { throw new IIOException(error(Errors.Keys.UnexpectedParameter_1, "images[2]")); } else { imageIndex = index[0]; } } } } } /* * From this point, we have enough information for creating the * NewGridCoverageReference instance. Before to close the reader, * check if the file is actually an aggregation of many smaller files. */ if (reader instanceof AggregatedImageStore) { aggregatedFiles = ((AggregatedImageStore) reader).getAggregatedFiles(imageIndex); } return new NewGridCoverageReference(database, reader, input, imageIndex, disposeReader); } /** * Returns the next elements (skipping {@code null} values) from the {@link #inputToAdd} * iterator, or {@code null} if we have reached the iteration end. * * @return The next input, or {@code null} if we have reached iteration end. */ public NewGridCoverageReference next() throws SQLException, IOException, FactoryException, DatabaseVetoException { while (inputToAdd.hasNext()) { final Object input = inputToAdd.next(); if (input != null) { return createEntry(input); } } return null; } /** * If the given entry is actually an aggregation of many files, returns the aggregated * elements. Otherwise returns the given entry in an array of length 1. The entry given * to this method must be the one returned by the last call to {@link #next()}. * * @param entry The last entry returned by {@link #next()}. * @return The aggregated elements. * @throws IIOException If an aggregated elements can not be created. * * @since 3.16 */ final NewGridCoverageReference[] aggregation(final NewGridCoverageReference entry) throws IIOException { if (aggregatedFiles == null) { return new NewGridCoverageReference[] {entry}; } final NewGridCoverageReference[] references = new NewGridCoverageReference[aggregatedFiles.size()]; int count = 0; int dateIndex = entry.imageIndex; for (final URI uri : aggregatedFiles) { // The URI should start with "file://". If there is no scheme, // assume that the string is directly a path in the native OS. final File file; if (uri.getScheme() == null) { file = new File(uri.toString()); } else try { file = new File(uri); } catch (IllegalArgumentException e) { throw new IIOException(e.getLocalizedMessage(), e); } references[count++] = new NewGridCoverageReference(entry, file, dateIndex++); } return references; } /** * Invoked by {@link WritableGridCoverageTable} after a {@link NewGridCoverageReference} * element has been fully constructed. * * @param isBefore {@code true} if the event is invoked before the change, * or {@code false} if the event occurs after the change. * @param value The entry which is added. * @throws DatabaseVetoException if {@code isBefore} is {@code true} and a listener vetoed * against the change. */ final void fireCoverageAdding(final boolean isBefore, final NewGridCoverageReference value) throws DatabaseVetoException { if (isBefore && controller != null) { controller.coverageAdding(new CoverageDatabaseEvent(listeners, isBefore, 1), value); } if (listeners != null) { listeners.fireChange(isBefore, 1, value); } } /** * Formats an error message for the given key. */ private String error(final short key, final Object argument) { return Errors.getResources(listeners.getLocale()).getString(key, argument); } }