/*
* 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.util.*;
import java.sql.Timestamp;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.awt.geom.Dimension2D;
import java.io.IOException;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.collection.RangeSet;
import org.geotoolkit.internal.EmptySortedSet;
import org.geotoolkit.internal.sql.table.Database;
import org.geotoolkit.internal.sql.table.SpatialDatabase;
import org.geotoolkit.internal.sql.table.BoundedSingletonTable;
import org.geotoolkit.internal.sql.table.CatalogException;
import org.geotoolkit.internal.sql.table.NoSuchTableException;
import org.geotoolkit.internal.sql.table.LocalCache;
import org.geotoolkit.internal.sql.table.Parameter;
import org.geotoolkit.internal.sql.table.QueryType;
import org.geotoolkit.internal.UnmodifiableArraySortedSet;
import org.geotoolkit.image.io.mosaic.TileManager;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* Connection to a table of grid coverages. This table builds references in the form of
* {@link GridCoverageReference} objects, which will defer the image loading until first
* needed. A {@code GridCoverageTable} can produce a list of available image intercepting
* a given {@linkplain #setEnvelope2D horizontal area} and {@linkplain #setTimeRange time range}.
*
* {@section Implementation note}
* For proper working of this class, the SQL query must sort entries by end time. If this
* condition is changed, then {@link GridCoverageEntry#equalsAsSQL} must be updated accordingly.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Sam Hiatt
* @version 3.15
*
* @since 3.10 (derived from Seagis)
* @module
*/
class GridCoverageTable extends BoundedSingletonTable<GridCoverageEntry> {
/**
* Amount of milliseconds in a day.
*/
static final long MILLIS_IN_DAY = 24*60*60*1000L;
/**
* The currently selected layer, or {@code null} if not yet set.
*/
private LayerEntry layer;
/**
* The currently selected series in the current layer, or {@code null} for auto-detect.
* This field is usually {@code null} since the public API works on layer as a whole.
* It is used only in a few cases where a caller needs to work on a specific series,
* and by {@link WritableGridCoverageTable} which need to associate new entries to a
* specific series.
*/
SeriesEntry specificSeries;
/**
* The table of grid geometries. Will be created only when first needed.
*/
private transient GridGeometryTable gridGeometryTable;
/**
* The table of tiles. Will be created only when first needed.
*/
private transient TileTable tileTable;
/**
* Comparator for selecting the "best" image when more than one is available in
* the spatio-temporal area of interest. Will be created only when first needed.
*/
private transient Comparator<GridCoverageReference> comparator;
/**
* Constructs a new {@code GridCoverageTable}.
*
* {@section Implementation note}
* This constructor actually expects an instance of {@link SpatialDatabase},
* but we have to keep {@link Database} in the method signature because this
* constructor is fetched by reflection.
*
* @param database The connection to the database.
*/
public GridCoverageTable(final Database database) {
this(new GridCoverageQuery((SpatialDatabase) database));
}
/**
* Constructs a new {@code GridCoverageTable} from the specified query.
*/
GridCoverageTable(final GridCoverageQuery query) {
// Method createIdentifier(...) expect the parameters to be in exactly that order.
super(query, new Parameter[] {query.bySeries, query.byFilename, query.byIndex},
query.byStartTime, query.byHorizontalExtent);
}
/**
* Creates a new instance having the same configuration than the given table.
* This is a copy constructor used for obtaining a new instance to be used
* concurrently with the original instance.
*
* @param table The table to use as a template.
*/
GridCoverageTable(final GridCoverageTable 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 GridCoverageTable clone() {
return new GridCoverageTable(this);
}
/**
* Invoked after constructor for initializing the {@link #envelope} field.
*/
@Override
protected CoverageEnvelope createEnvelope() {
return new CoverageEnvelope((SpatialDatabase) getDatabase()) {
@Override void fireStateChanged(final String property) {
super.fireStateChanged(property);
GridCoverageTable.this.fireStateChanged(property);
}
};
}
/**
* Sets the layer as a string.
*
* @param layer The layer name.
* @throws SQLException if the layer can not be set to the given value.
*/
public final void setLayer(final String layer) throws SQLException {
ensureNonNull("layer", layer);
if (!layer.equals(getLayer())) {
final LayerTable table = getDatabase().getTable(LayerTable.class);
this.layer = table.getEntry(layer);
table.release();
fireStateChanged("Layer");
}
}
/**
* Returns the name of the current layer, or {@code null} if none.
*/
public final String getLayer() {
final LayerEntry layer = this.layer;
return (layer != null) ? layer.getName() : null;
}
/**
* Sets the layer as an entry.
*/
final void setLayerEntry(final LayerEntry layer) {
ensureNonNull("layer", layer);
if (!layer.equals(this.layer)) {
this.layer = layer;
fireStateChanged("Layer");
}
}
/**
* Returns the layer for the coverages in this table.
*
* @param required If {@code true}, then the layer is required to be non-null.
* @throws CatalogException if the layer is not set and {@code required} is {@code true}.
*/
final LayerEntry getLayerEntry(final boolean required) throws CatalogException {
final LayerEntry layer = this.layer;
if (layer == null && required) {
throw new CatalogException(errors().getString(Errors.Keys.NoLayerSpecified));
}
return layer;
}
/**
* Returns the currently selected series. If {@link #setSeries(SeriesEntry)} has been invoked,
* the given series is returned. Otherwise if the current layer contains exactly one series,
* then that series is returned since there is no ambiguity. Otherwise an exception is thrown.
*
* @return The series for the {@linkplain #getLayerEntry() current layer}.
* @throws SQLException if no series can be inferred from the current layer.
*/
private SeriesEntry getSeries() throws SQLException {
if (specificSeries != null) {
return specificSeries;
}
final Iterator<SeriesEntry> iterator = getLayerEntry(true).getSeries().iterator();
if (iterator.hasNext()) {
final SeriesEntry series = iterator.next();
if (!iterator.hasNext()) {
return series;
}
}
throw new CatalogException(errors().getString(Errors.Keys.NoSeriesSpecified));
}
/**
* Returns the {@link GridGeometryTable} instance, creating it if needed. This method is not
* private because it is also used by {@link LayerEntry#getCountByExtent()}, but this is okay
* if used in the same thread than this {@code GridCoverageTable} instance.
*/
final GridGeometryTable getGridGeometryTable() throws NoSuchTableException {
GridGeometryTable table = gridGeometryTable;
if (table == null) {
gridGeometryTable = table = getDatabase().getTable(GridGeometryTable.class);
}
return table;
}
/**
* Returns the {@link TileTable} instance, creating it if needed.
*/
private TileTable getTileTable() throws NoSuchTableException {
TileTable table = tileTable;
if (table == null) {
tileTable = table = getDatabase().getTable(TileTable.class);
}
return table;
}
/**
* Returns the two-dimensional coverages that intercept the
* {@linkplain #getEnvelope current spatio-temporal envelope}.
*
* @return List of coverages in the current envelope of interest.
* @throws SQLException if an error occurred while reading the database.
*/
@Override
public final Set<GridCoverageEntry> getEntries() throws SQLException {
final Dimension2D resolution = envelope.getPreferredResolution();
final Set<GridCoverageEntry> entries = super.getEntries();
final List<GridCoverageEntry> filtered = new ArrayList<>(entries.size());
loop: for (final GridCoverageEntry newEntry : entries) {
/*
* If there is many entries with the same spatio-temporal envelope but different
* resolution, keep the one with a resolution close to the requested one and
* remove the other entries.
*/
for (int i=filtered.size(); --i>=0;) {
final GridCoverageEntry oldEntry = filtered.get(i);
if (!oldEntry.equalsAsSQL(newEntry)) {
// Entries not equal according the "ORDER BY" clause.
break;
}
final GridCoverageEntry coarseResolution = oldEntry.selectCoarseResolution(newEntry);
if (coarseResolution != null) {
// Two entries has the same spatio-temporal coordinates.
if (coarseResolution.hasEnoughResolution(resolution)) {
// The entry with the lowest resolution is enough.
filtered.set(i, coarseResolution);
} else if (coarseResolution == oldEntry) {
// No entry has enough resolution;
// keep the one with the finest resolution.
filtered.set(i, newEntry);
}
continue loop;
}
}
filtered.add(newEntry);
}
entries.retainAll(filtered);
return entries;
}
/**
* Returns one of the two-dimensional coverages that intercept the {@linkplain #getEnvelope()
* current spatio-temporal envelope}. If more than one coverage intercept the envelope (i.e.
* if {@link #getEntries()} returns a set containing at least two elements), then a coverage
* will be selected using the default {@link GridCoverageComparator}.
*
* @return A coverage intercepting the given envelope, or {@code null} if none.
* @throws SQLException if an error occurred while reading the database.
*/
public final GridCoverageEntry getEntry() throws SQLException {
final Iterator<GridCoverageEntry> entries = getEntries().iterator();
GridCoverageEntry best = null;
if (entries.hasNext()) {
best = entries.next();
if (entries.hasNext()) {
Comparator<GridCoverageReference> comparator = this.comparator;
if (comparator == null) {
comparator = new GridCoverageComparator(envelope);
this.comparator = comparator;
}
do {
final GridCoverageEntry entry = entries.next();
if (comparator.compare(entry, best) <= -1) {
best = entry;
}
} while (entries.hasNext());
}
}
return best;
}
/**
* Returns an element for the given identifier.
*
* @param identifier The filename as a {@link String} (in which case the series is
* the {@linkplain #getSeries() current one} and the image index is 1) or a
* {@link GridCoverageIdentifier} with all primary key values.
*/
@Override
public GridCoverageEntry getEntry(Comparable<?> identifier) throws SQLException {
return super.getEntry(toGridCoverageIdentifier(identifier));
}
/**
* Tests if the given entry exists. This method does not attempt to create
* the entry and doesn't check if the entry is valid.
*
* @param identifier The filename as a {@link String} (in which case the series is
* the {@linkplain #getSeries() current one} and the image index is 1) or a
* {@link GridCoverageIdentifier} with all primary key values.
*/
@Override
public boolean exists(Comparable<?> identifier) throws SQLException {
return super.exists(toGridCoverageIdentifier(identifier));
}
/**
* Deletes the given entry.
*
* @param identifier The filename as a {@link String} (in which case the series is
* the {@linkplain #getSeries() current one} and the image index is 1) or a
* {@link GridCoverageIdentifier} with all primary key values.
*/
@Override
public int delete(Comparable<?> identifier) throws SQLException {
return super.delete(toGridCoverageIdentifier(identifier));
}
/**
* Returns the set of dates when a coverage is available. Only the images in
* the currently {@linkplain #getEnvelope selected envelope} are considered.
*
* @return The set of dates.
* @throws SQLException if an error occurred while reading the database.
*/
public final SortedSet<Date> getAvailableTimes() throws SQLException {
final Set<Long> dates = new HashSet<>();
final GridCoverageQuery query = (GridCoverageQuery) super.query;
final LocalCache lc = getLocalCache();
synchronized (lc) {
final LocalCache.Stmt ce = getStatement(lc, QueryType.AVAILABLE_DATA);
final int startTimeIndex = indexOf(query.startTime);
final int endTimeIndex = indexOf(query.endTime);
final Calendar calendar = getCalendar(lc);
final PreparedStatement statement = ce.statement;
try (ResultSet results = statement.executeQuery()) {
while (results.next()) {
final Date startTime = results.getTimestamp(startTimeIndex, calendar);
final Date endTime = results.getTimestamp( endTimeIndex, calendar);
final long time;
if (startTime != null) {
if (endTime != null) {
time = (startTime.getTime() + endTime.getTime()) >>> 1;
} else {
time = startTime.getTime();
}
} else if (endTime != null) {
time = endTime.getTime();
} else {
continue;
}
dates.add(time);
}
}
release(lc, ce);
}
if (dates.isEmpty()) {
return EmptySortedSet.INSTANCE;
}
return new UnmodifiableArraySortedSet.Date(dates);
}
/**
* Returns the range of date for available images.
*
* @param addTo If non-null, the set where to add the time range of available coverages.
* @return The time range of available coverages. This method returns {@code addTo} if it
* was non-null or a new object otherwise.
* @throws SQLException if an error occurred while reading the database.
*/
public final RangeSet<Date> getAvailableTimeRanges(RangeSet<Date> addTo) throws SQLException {
final GridCoverageQuery query = (GridCoverageQuery) super.query;
final int startTimeIndex = indexOf(query.startTime);
final int endTimeIndex = indexOf(query.endTime);
long lastEndTime = Long.MIN_VALUE;
final LocalCache lc = getLocalCache();
synchronized (lc) {
final Calendar calendar = getCalendar(lc);
final LocalCache.Stmt ce = getStatement(lc, QueryType.AVAILABLE_DATA);
final PreparedStatement statement = ce.statement;
try (ResultSet results = statement.executeQuery()) {
final LayerEntry layer = this.layer; // Don't use getLayerEntry() because we want to accept null.
final long timeInterval = (layer != null) ? Math.round(layer.timeInterval * MILLIS_IN_DAY) : 0;
if (addTo == null) {
addTo = RangeSet.create(Date.class, true, false);
}
while (results.next()) {
final Date startTime = results.getTimestamp(startTimeIndex, calendar);
final Date endTime = results.getTimestamp( endTimeIndex, calendar);
if (startTime != null && endTime != null) {
final long lgEndTime = endTime.getTime();
final long checkTime = lgEndTime - timeInterval;
if (checkTime <= lastEndTime && checkTime < startTime.getTime()) {
/*
* Use case: some layer may produce images every 24 hours, but declare
* a time range spaning only 12 hours for each image. We don't want to
* consider the 12 remaining hours as a hole in data availability. If
* the 'timeInterval' is set to "1 day", then we merge the time range
* of consecutive images.
*/
startTime.setTime(checkTime);
}
lastEndTime = lgEndTime;
addTo.add(startTime, endTime);
}
}
}
release(lc, ce);
}
return addTo;
}
/**
* Configures the specified query. This method is invoked automatically after this table
* {@linkplain #fireStateChanged changed its state}.
*
* @throws SQLException if a SQL error occurred while configuring the statement.
*/
@Override
protected final void configure(final LocalCache lc, final QueryType type, final PreparedStatement statement)
throws SQLException
{
super.configure(lc, type, statement);
final GridCoverageQuery query = (GridCoverageQuery) super.query;
int index = query.byLayer.indexOf(type);
if (index != 0) {
statement.setString(index, getLayerEntry(true).getName());
}
index = query.bySeries.indexOf(type);
if (index != 0) {
final SeriesEntry series = getSeries();
assert getLayerEntry(true).getSeries().contains(series) : series;
statement.setInt(index, series.getIdentifier());
}
}
/**
* Creates an identifier for the current row in the given result set. This method expects
* that {@code pkIndices} are for the "series", "filename" and "index" columns, in that
* order. This order is determined by the constructor.
*/
@Override
protected final Comparable<?> createIdentifier(final ResultSet results, final int[] pkIndices)
throws SQLException
{
if (pkIndices.length != 3) {
/*
* pkIndices.length should always be 3. If not, we have a bug. Invoking
* the super-class method will intentionally throw an exception (unless
* the length is 1, in which case the super-class can manage).
*/
return super.createIdentifier(results, pkIndices);
}
final int seriesID = results.getInt (pkIndices[0]);
final String filename = results.getString(pkIndices[1]);
final short index = results.getShort (pkIndices[2]); // We expect 0 if null.
/*
* Gets the SeriesEntry in which this coverage is declared. The entry should be available
* from the layer HashMap. If not, we will query the SeriesTable as a fallback, but there
* is probably a bug (unless the table is queried immediately after the insertion of new
* entries, and the LayerEntry has not been recreated from a refreshen LayerTable).
*/
final LayerEntry layer = getLayerEntry(true);
SeriesEntry series = layer.getSeries(seriesID);
if (series == null) { // Should not happen, but be lenient if it happen anyway.
final SeriesTable table = getDatabase().getTable(SeriesTable.class);
table.setLayer(getLayer());
series = table.getEntry(seriesID);
table.release();
}
/*
* We need to include the altitude in the identifier (since requests for different
* altitude result in different coverages), but altitude is not an explicit column
* in the 'GridCoverages' table. We need to compute it from the list of vertical
* ordinate values.
*/
final GridCoverageQuery query = (GridCoverageQuery) super.query;
final int extent = results.getInt(indexOf(query.spatialExtent));
final GridGeometryEntry geometry = getGridGeometryTable().getEntry(extent);
final NumberRange<?> verticalRange = envelope.getVerticalRange();
final double z = 0.5*(verticalRange.getMinDouble() + verticalRange.getMaxDouble());
return new GridCoverageIdentifier(series, filename, index,
geometry.indexOfNearestAltitude(z), geometry);
}
/**
* Creates an entry from the current row in the specified result set.
*
* @throws SQLException if an error occurred while reading the database.
*/
@Override
protected final GridCoverageEntry createEntry(final LocalCache lc, final ResultSet results, final Comparable<?> identifier)
throws SQLException
{
final GridCoverageIdentifier id = (GridCoverageIdentifier) identifier;
final Calendar calendar = getCalendar(lc);
final GridCoverageQuery query = (GridCoverageQuery) super.query;
final Timestamp startTime = results.getTimestamp(indexOf(query.startTime), calendar);
final Timestamp endTime = results.getTimestamp(indexOf(query.endTime), calendar);
/*
* Complete the geometry if it was null. This may happen when toGridCoverageIdentifier
* (below) is invoked, which usually don't happen in typical GridCoverageTable usage.
*/
if (id.geometry == null) {
id.geometry = getGridGeometryTable().getEntry(results.getInt(indexOf(query.spatialExtent)));
}
/*
* If the layer is tiled, read the tiles.
*/
final LayerEntry layer = getLayerEntry(true);
Boolean isTiled = layer.isTiled;
if (isTiled == null) {
layer.isTiled = isTiled = getTileTable().exists(layer);
}
TileManager[] managers = null;
if (isTiled) try {
managers = getTileTable().getTiles(layer, startTime, endTime, id.geometry.getHorizontalSRID());
} catch (IOException e) {
throw new CatalogException(e);
}
return new GridCoverageEntry(id, startTime, endTime, managers, null);
}
/**
* Converts the given identifier into an instance of {@link GridCoverageIdentifier}
* if possible. Arbitrary values are used for unspecified parameters like image and
* <var>z</var> index.
* <p>
* This method is invoked indirectly mostly for testing purpose. It is not expected
* to be invoked in typical {@code GridCoverageTable} usage.
*
* @param identifier The identifier, or {@code null}.
* @return The identifier to use, or {@code null} if the given identifier was null.
*/
private Comparable<?> toGridCoverageIdentifier(Comparable<?> identifier) throws SQLException {
if (identifier instanceof CharSequence) {
identifier = new GridCoverageIdentifier(getSeries(), identifier.toString(), (short) 1);
}
return identifier;
}
/**
* Returns the number of coverages for the given series. The keys of the returned map are
* the identifiers found for the current series. The values are the number of occurrences.
*
* @param series The series for which to count
* @param groupByExtent {@code true} for grouping by extents.
* @return The number of records by identifier.
*/
final Map<Integer,Integer> count(final SeriesEntry series, final boolean groupByExtent) throws SQLException {
final GridCoverageQuery query = (GridCoverageQuery) this.query;
final Map<Integer,Integer> count = new HashMap<>();
final LocalCache lc = getLocalCache();
synchronized (lc) {
final SeriesEntry oldSeries = specificSeries;
specificSeries = series;
try {
final LocalCache.Stmt ce = getStatement(lc, QueryType.COUNT,
groupByExtent ? query.spatialExtent : query.series);
final PreparedStatement stmt = ce.statement;
try (ResultSet results = stmt.executeQuery()) {
while (results.next()) {
final Integer k = results.getInt(1);
final int c = results.getInt(2);
final Integer p = count.put(k, c);
if (p != null) {
count.put(k, p+c); // Should not happen, but let be paranoiac.
}
}
}
release(lc, ce);
} finally {
specificSeries = oldSeries;
}
}
return count;
}
/**
* {@inheritDoc}
*/
@Override
protected void fireStateChanged(final String property) {
if (!"PreferredResolution".equals(property)) {
comparator = null;
}
super.fireStateChanged(property);
}
}