/*
* 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.internal.sql.table;
import java.util.Date;
import java.util.Calendar;
import java.sql.Timestamp;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLDataException;
import java.sql.PreparedStatement;
import java.awt.geom.Rectangle2D;
import org.geotoolkit.util.DateRange;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.Envelope2D;
import org.apache.sis.geometry.GeneralEnvelope;
import org.geotoolkit.display.shape.XRectangle2D;
import org.geotoolkit.coverage.sql.CoverageEnvelope;
import org.apache.sis.referencing.CommonCRS;
/**
* Base class for tables with a {@code getEntry(...)} method restricted to the elements contained in
* some spatio-temporal bounding box. The bounding box is defined either by a {@link CoverageEnvelope}
* expressed in the {@linkplain CoverageEnvelope#getSpatioTemporalCRS CRS of this table}, or by a
* combination of {@linkplain CoverageEnvelope#getHorizontalRange() horizontal envelope},
* {@link #getVerticalRange() vertical range} and {@linkplain CoverageEnvelope#getTimeRange()
* time range} expressed in standard CRS.
*
* {@section Convention}
* For every envelopes or ranges used by this class, the lower and upper bounds are both inclusive.
* This is done that way for consistency with the envelope computed by {@link #trimEnvelope()}.
*
* @param <E> The kind of entries to be created by this table.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.15
*
* @since 3.10 (derived from Seagis)
* @module
*/
public abstract class BoundedSingletonTable<E extends Entry> extends SingletonTable<E> {
/**
* If no minimal and maximal <var>x</var> or <var>y</var> value was supplied, the value
* to use. This is used because the {@code 'infinity'} value doesn't seem to work.
* <p>
* Note that default values exist in the time dimension as well. Search for usage of
* {@code DEFAULT_LIMIT} to find the method.
*/
private static final Rectangle2D DEFAULT_LIMIT =
XRectangle2D.createFromExtremums(-1E+12, -1E+12, 1E+12, 1E+12);
/**
* The parameter to use for looking an element by time range, or {@code null} if none.
*/
private final Parameter byTimeRange;
/**
* The parameter to use for looking an element by horizontal spatial extent,
* or {@code null} if none.
*/
private final Parameter bySpatialExtent;
/**
* The spatio-temporal extent of the query. Getter and setter methods can be invoked
* directly on this instance.
*/
public final CoverageEnvelope envelope;
/**
* {@code true} if the {@link #trimEnvelope} method already shrinked the
* {@linkplain #getEnvelope spatio-temporal envelope} for this table.
*/
private boolean trimmed;
/**
* Creates a new table using the specified query. The query given in argument should be some
* subclass with {@link Query#addColumn addColumn} and {@link Query#addParameter addParameter}
* methods invoked in its constructor.
*
* @param query The query to use for this table.
* @param pkParam The parameter for looking an element by name, or {@code null} if none.
* @param byTimeRange The parameter to use for looking an element by time range.
* @param bySpatialExtent The parameter to use for looking an element by horizontal spatial extent.
* @throws IllegalArgumentException if the specified parameters are not one of those
* declared for {@link QueryType#SELECT}.
*/
protected BoundedSingletonTable(final Query query, final Parameter[] pkParam,
final Parameter byTimeRange, final Parameter bySpatialExtent)
{
super(query, pkParam);
this.byTimeRange = byTimeRange;
this.bySpatialExtent = bySpatialExtent;
envelope = createEnvelope();
}
/**
* Creates a new table connected to the same {@linkplain #getDatabase database} and using
* the same {@linkplain #query query} than the specified table. Subclass constructors should
* not modify the query, since it is shared.
* <p>
* In addition, the new table is initialized to the same spatio-temporal envelope and the
* same {@linkplain #getSpatioTemporalCRS coordinate reference system} than the
* specified table.
*
* @param table The table to use as a template.
*/
protected BoundedSingletonTable(final BoundedSingletonTable<E> table) {
super(table);
byTimeRange = table.byTimeRange;
bySpatialExtent = table.bySpatialExtent;
envelope = createEnvelope();
}
/**
* Invoked after constructor for initializing the {@link #envelope} field.
*
* @return An initially infinite coverage extent suitable for this table.
*/
protected abstract CoverageEnvelope createEnvelope();
/**
* Shrinks the {@linkplain #getEnvelope() spatio-temporal envelope} to a smaller envelope
* containing all the elements to be returned by this table. This method iterates over
* the elements that intercept the envelope specified by {@code setXXX(...)} methods.
* Then the envelope is altered in such a way that the {@code getXXX(...)} method returns
* an identical or smaller envelope intercepting the same set of elements.
*
* @throws SQLException if an error occurred while reading the database.
*/
public void trimEnvelope() throws SQLException {
if (trimmed) {
return;
}
final QueryType type = QueryType.BOUNDING_BOX;
final int timeColumn = (byTimeRange != null) ? byTimeRange .column.indexOf(type) : 0;
final int bboxColumn = (bySpatialExtent != null) ? bySpatialExtent.column.indexOf(type) : 0;
if (timeColumn != 0 || bboxColumn != 0) {
final LocalCache lc = getLocalCache();
synchronized (lc) {
final LocalCache.Stmt ce = getStatement(lc, type);
final PreparedStatement statement = ce.statement;
try (ResultSet results = statement.executeQuery()) {
while (results.next()) { // Should contains only one record.
if (timeColumn != 0) {
final Calendar calendar = getCalendar(lc);
final Date tMin = results.getTimestamp(timeColumn, calendar); // NOSONAR: timeColumn can't be 0.
final Date tMax = results.getTimestamp(timeColumn+1, calendar);
// Computes the intersection with the time range that we found.
Date t;
final DateRange range = envelope.getTimeRange();
if ((t = range.getMinValue()) != null && t.after (tMin)) tMin.setTime(t.getTime());
if ((t = range.getMaxValue()) != null && t.before(tMax)) tMax.setTime(t.getTime());
envelope.setTimeRange(tMin, tMax);
}
if (bboxColumn != 0) {
final String bbox = results.getString(bboxColumn); // NOSONAR: bboxColumn can't be 0.
if (bbox == null) {
continue;
}
final GeneralEnvelope ge;
try {
ge = new GeneralEnvelope(bbox);
} catch (RuntimeException e) {
throw new IllegalRecordException(e, this, results, bboxColumn, null);
}
final XRectangle2D region = XRectangle2D.createFromExtremums(
ge.getMinimum(0), ge.getMinimum(1),
ge.getMaximum(0), ge.getMaximum(1));
region.intersect(envelope.getHorizontalRange());
envelope.setHorizontalRange(region);
}
}
}
release(lc, ce);
fireStateChanged("Envelope");
}
}
trimmed = true;
}
/**
* Invoked automatically for a newly created statement or when this table
* {@linkplain #fireStateChanged changed its state}. The default implementation
* set the parameter values to the spatio-temporal bounding box.
*
* @param lc The {@link #getLocalCache()} value.
* @param type The query type (mat be {@code null}).
* @param statement The statement to configure (never {@code null}).
* @throws SQLDataException If the {@linkplain #envelope} is invalid.
* @throws SQLException If an other kind of SQL error occurred while configuring the statement.
*/
@Override
protected void configure(final LocalCache lc, final QueryType type, final PreparedStatement statement)
throws SQLDataException, SQLException
{
super.configure(lc, type, statement);
if (byTimeRange != null) {
final int index = byTimeRange.indexOf(type);
if (index != 0) {
final DateRange range = envelope.getTimeRange();
Date tMin = range.getMinValue();
Date tMax = range.getMaxValue();
/*
* The default for minimum and maximum values are arbitrary, but we need to
* provide something. It seems that 'infinity' value doesn't work through JDBC.
*
* TODO: Revisit if we find some way to specify 'infinity' with future JDBC drivers.
*/
if (tMin == null) {
tMin = CommonCRS.Temporal.JULIAN.datum().getOrigin();
}
if (tMax == null) {
tMax = new Date();
}
final Calendar calendar = getCalendar(lc);
statement.setTimestamp(index, new Timestamp(tMax.getTime()), calendar);
statement.setTimestamp(index+1, new Timestamp(tMin.getTime()), calendar);
}
}
if (bySpatialExtent != null) {
final int index = bySpatialExtent.indexOf(type);
if (index != 0) {
final Envelope2D env = new Envelope2D();
Rectangle2D.intersect(envelope.getHorizontalRange(), DEFAULT_LIMIT, env);
final String wkt;
try {
wkt = Envelopes.toPolygonWKT(env);
} catch (IllegalArgumentException e) {
throw new SQLDataException(e.getLocalizedMessage(), e);
}
statement.setString(index, wkt);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void fireStateChanged(final String property) {
if (!"PreferredResolution".equals(property)) {
trimmed = false;
}
super.fireStateChanged(property);
}
}