/*
* 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.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.sql.Types;
import java.sql.Timestamp;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.net.URL;
import java.net.URI;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.Collection;
import javax.imageio.ImageReader;
import org.opengis.util.FactoryException;
import org.geotoolkit.util.DateRange;
import org.geotoolkit.image.io.mosaic.Tile;
import org.geotoolkit.internal.sql.table.Database;
import org.geotoolkit.internal.sql.table.QueryType;
import org.geotoolkit.internal.sql.table.LocalCache;
import org.geotoolkit.internal.sql.table.SpatialDatabase;
/**
* A grid coverage table with write capabilities. This class can be used in order to insert new
* image in the database. Note that adding new records in the {@code "GridCoverages"} table may
* imply adding new records in dependent tables like {@code "GridGeometries"}. This class may
* add new records, but will never modify existing records.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Antoine Hnawia (IRD)
* @version 3.16
*
* @since 3.12 (derived from Seagis)
* @module
*/
final class WritableGridCoverageTable extends GridCoverageTable {
/**
* The object to use for writing in the {@code "Tiles"} table. Created only if needed.
* This is used for adding tiles (as opposed to ordinary images), and is encapsulated
* in this {@code WritableGridCoverageTable} because we will also need to write an entry
* in the {@code "GridCoverages"} table for the mosaic as a whole.
*/
private transient WritableGridCoverageTable tilesTable;
/**
* Constructs a new {@code WritableGridCoverageTable}.
*
* @param database The connection to the database.
*/
public WritableGridCoverageTable(final Database database) {
super(database);
}
/**
* Constructs a new {@code WritableGridCoverageTable} from the specified query.
*/
private WritableGridCoverageTable(final GridCoverageQuery query) {
super(query);
}
/**
* Constructs a new {@code WritableGridCoverageTable} with the same initial configuration
* than the specified table.
*
* @param table The table to use as a template.
*/
private WritableGridCoverageTable(final WritableGridCoverageTable table) {
super(table);
}
/**
* Returns a copy of this table. This is a copy constructor used for obtaining
* a new instance to be used concurrently with the original instance.
*/
@Override
protected WritableGridCoverageTable clone() {
return new WritableGridCoverageTable(this);
}
/**
* Adds entries inferred from the specified image inputs. The inputs shall be any of the
* following instances:
* <p>
* <ul>
* <li>{@link File}, {@link URL}, {@link URI} or {@link String} instances.</li>
*
* <li>{@link Tile} instances, which will be added in the {@code "GridCoverages"} table
* like any other kind of input processed by this method. For adding tiles in the
* {@code "Tiles"} table, use the {@link #addTiles(Collection)} method instead.</li>
*
* <li>{@link ImageReader} instances with the {@linkplain ImageReader#getInput() input}
* set and {@linkplain ImageReader#getImageMetadata metadata} conform to the Geotk
* {@linkplain SpatialMetadata spatial metadata}. The reader input shall be one of
* the above-cited instances. If this is not possible (for example because a
* {@link javax.imageio.stream.ImageInputStream} is required), consider wrapping
* the {@link javax.imageio.spi.ImageReaderSpi} and the input in a {@link Tile}
* instance.</li>
* </ul>
* <p>
* This method will typically not read the full image, but only the required metadata.
*
* @param inputs The image inputs.
* @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 method.
* @param controller An optional controller to invoke before the listeners, or {@code null}.
* @return The number of images inserted.
* @throws SQLException If an error occurred while querying the database.
* @throws IOException If an I/O operation was required and failed.
*/
public int addEntries(final Collection<?> inputs,
final CoverageDatabase listeners,
final CoverageDatabaseController controller)
throws SQLException, IOException, FactoryException, DatabaseVetoException
{
int count = 0;
if (!inputs.isEmpty()) {
boolean success = false;
final NewGridCoverageIterator iterator = new NewGridCoverageIterator(listeners,
controller, (SpatialDatabase) getDatabase(), inputs);
final SeriesEntry oldSeries = specificSeries;
final LocalCache lc = getLocalCache();
synchronized (lc) {
transactionBegin(lc);
try {
count = addEntries(lc, iterator);
success = true; // Must be the very last line in the try block.
} finally {
specificSeries = oldSeries;
transactionEnd(lc, success);
}
}
}
return count;
}
/**
* Adds entries without the protection provided by the database rollback mechanism.
* The synchronization, commit or rollback must be performed by the caller.
*
* @return The number of images inserted.
*/
private int addEntries(final LocalCache lc, final NewGridCoverageIterator entries)
throws SQLException, IOException, FactoryException, DatabaseVetoException
{
int count = 0;
final GridCoverageQuery query = (GridCoverageQuery) this.query;
final Database database = getDatabase();
final Calendar calendar = getCalendar(lc);
final LocalCache.Stmt ce = getStatement(lc, QueryType.INSERT);
final PreparedStatement statement = ce.statement;
final GridGeometryTable gridTable = database.getTable(GridGeometryTable.class);
final FormatTable formatTable = database.getTable(FormatTable.class);
final SeriesTable seriesTable = database.getTable(SeriesTable.class);
seriesTable.setLayer(getLayer());
final int bySeries = indexOf(query.series);
final int byFilename = indexOf(query.filename);
final int byIndex = indexOf(query.index);
final int byStartTime = indexOf(query.startTime);
final int byEndTime = indexOf(query.endTime);
final int byExtent = indexOf(query.spatialExtent);
final int byDx = (query.dx != null) ? query.dx.indexOf(QueryType.INSERT) : 0;
final int byDy = (query.dy != null) ? query.dy.indexOf(QueryType.INSERT) : 0;
final boolean explicitTranslate = (byDx != 0 && byDy != 0);
NewGridCoverageReference mainEntry;
while ((mainEntry = entries.next()) != null) {
/*
* Notifies the controller (if any), then the listeners (if any) after the
* NewGridCoverageReference entry has been fully initialized. The controller
* may change the values. Then creates the format, assuming that every entry
* will use the same format.
*/
entries.fireCoverageAdding(true, mainEntry);
mainEntry.format = formatTable.findOrCreate(mainEntry.format,
mainEntry.bestFormat.imageFormat, mainEntry.sampleDimensions);
/*
* Gets the metadata of interest. The metadata should contains at least the image
* envelope and CRS. If it doesn't, then we will use the table envelope as a fall
* back. It defaults to the whole Earth in WGS 84 geographic coordinates, but the
* user can set an other value using the setEnvelope(...) method.
*/
final Rectangle imageBounds = mainEntry.imageBounds;
final AffineTransform gridToCRS = mainEntry.gridToCRS;
if (!explicitTranslate && (imageBounds.x != 0 || imageBounds.y != 0)) {
// If the translation can not be recorded explicitly in the database, then we
// need to apply it on the affine transform. Note that we really want to update
// the NewGridCoverageReference field, in order to notify the listeners with an
// accurate AffineTransform after the change.
gridToCRS.translate(imageBounds.x, imageBounds.y);
}
final int extent = gridTable.findOrCreate(imageBounds.getSize(), gridToCRS,
mainEntry.horizontalSRID, mainEntry.verticalValues, mainEntry.verticalSRID);
/*
* If the entry is an aggregation, actually inserts the aggregated elements instead
* than the aggregation. This loop assumes that all aggregated element use the same
* format and the same extent than the aggregation. This assumption is burned in the
* NewGridCoverageReference(NewGridCoverageReference, ...) constructor. If this
* assumption doesn't hold anymore in a future version, then the calculation of
* 'entry.format' and 'extent' above need to move inside the loop.
*/
for (final NewGridCoverageReference entry : entries.aggregation(mainEntry)) {
/*
* Create the series if it does not exist. Note that new series
* may be created if the entries are in different directories.
*/
final String directory = (entry.path != null) ? entry.path.toString() : "";
final int seriesID = seriesTable.findOrCreate(directory, entry.extension, entry.format);
specificSeries = seriesTable.getEntry(seriesID);
/*
* Adds the entries for each image found in the file.
* There is often only one image per file, but not always.
*/
statement.setInt (bySeries, specificSeries.getIdentifier());
statement.setString(byFilename, entry.filename);
statement.setInt (byExtent, extent);
if (explicitTranslate) {
final Rectangle translate = entry.imageBounds;
statement.setInt(byDx, translate.x); // NOSONAR: byDx can not be zero.
statement.setInt(byDy, translate.y); // NOSONAR: byDy can not be zero.
}
final DateRange[] dateRanges = entry.dateRanges;
int imageIndex = entry.imageIndex;
if (dateRanges == null) {
statement.setInt (byIndex, ++imageIndex);
statement.setNull(byStartTime, Types.TIMESTAMP);
statement.setNull(byEndTime, Types.TIMESTAMP);
if (updateSingleton(statement)) count++;
} else for (final DateRange dateRange : dateRanges) {
final Date startTime = dateRange.getMinValue();
final Date endTime = dateRange.getMaxValue();
statement.setInt (byIndex, ++imageIndex);
statement.setTimestamp(byStartTime, new Timestamp(startTime.getTime()), calendar);
statement.setTimestamp(byEndTime, new Timestamp(endTime .getTime()), calendar);
if (updateSingleton(statement)) count++;
}
}
/*
* Notifies the listeners that the entries have been added.
*/
entries.fireCoverageAdding(false, mainEntry);
}
seriesTable.release();
formatTable.release();
gridTable.release();
return count;
}
/**
* Adds the specified tiles in the {@code "Tiles"} table.
*
* @param tiles The tiles to insert.
* @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 method.
* @param controller An optional controller to invoke before the listeners, or {@code null}.
* @throws SQLException If an error occurred while querying the database.
* @throws IOException If an I/O operation was required and failed.
*/
public void addTiles(final Collection<Tile> tiles,
final CoverageDatabase listeners,
final CoverageDatabaseController controller)
throws SQLException, IOException, FactoryException, DatabaseVetoException
{
if (tilesTable == null) {
// Uses the special GridCoverageQuery constructor for insertions in "Tiles" table.
tilesTable = new WritableGridCoverageTable(new GridCoverageQuery((SpatialDatabase) getDatabase(), true));
}
tilesTable.setLayer(getLayer());
tilesTable.specificSeries = specificSeries;
tilesTable.addEntries(tiles, listeners, controller);
}
}