/* * 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.sql.Array; import java.sql.Types; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.PreparedStatement; import java.sql.SQLNonTransientException; import java.awt.Dimension; import java.awt.geom.AffineTransform; import java.util.Arrays; import java.util.logging.Level; import org.opengis.util.FactoryException; import org.opengis.metadata.Identifier; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransformFactory; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.collection.WeakHashSet; import org.geotoolkit.internal.sql.table.Column; 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.SingletonTable; import org.geotoolkit.internal.sql.table.IllegalRecordException; import org.geotoolkit.internal.sql.table.SpatialDatabase; import org.geotoolkit.metadata.Citations; import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.referencing.factory.GeodeticAuthorityFactory; import org.apache.sis.referencing.factory.IdentifiedObjectFinder; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.geotoolkit.resources.Errors; import static java.lang.reflect.Array.getLength; import static java.lang.reflect.Array.getDouble; /** * Connection to a table of grid geometries. * * @author Martin Desruisseaux (IRD, Geomatys) * @author Antoine Hnawia (IRD) * @version 3.15 * * @since 3.10 (derived from Seagis) * @module */ final class GridGeometryTable extends SingletonTable<GridGeometryEntry> { /** * A set of CRS descriptions created up to date. Cached because we * will typically have many grid geometries using the same set of CRS. */ private final WeakHashSet<SpatialRefSysEntry> gridCRS; /** * Constructs a new {@code GridGeometryTable}. * * @param connection The connection to the database. */ public GridGeometryTable(final Database database) { this(new GridGeometryQuery(database)); } /** * Constructs a new {@code GridGeometryTable} from the specified query. */ private GridGeometryTable(final GridGeometryQuery query) { super(query, query.byIdentifier); gridCRS = new WeakHashSet<>(SpatialRefSysEntry.class); } /** * 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. */ private GridGeometryTable(final GridGeometryTable table) { super(table); gridCRS = table.gridCRS; } /** * 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 GridGeometryTable clone() { return new GridGeometryTable(this); } /** * Returns a CRS identifier for the specified CRS. The given CRS should appears in the PostGIS * {@code "spatial_ref_sys"} table. The returned value is a primary key in the same table. * * @param crs The CRS to search. * @return The identifier for the given CRS, or 0 if none. * @throws FactoryException if an error occurred while searching for the CRS. */ public int getSRID(final CoordinateReferenceSystem crs) throws FactoryException { final SpatialDatabase database = (SpatialDatabase) getDatabase(); final GeodeticAuthorityFactory factory = (GeodeticAuthorityFactory) database.getCRSAuthorityFactory(); final IdentifiedObjectFinder finder = factory.newIdentifiedObjectFinder(); final Identifier srid = IdentifiedObjects.getIdentifier(finder.findSingleton(crs), Citations.POSTGIS); if (srid == null) { return 0; } final String code = srid.getCode(); try { return Integer.parseInt(code); } catch (NumberFormatException e) { throw new FactoryException(Errors.format(Errors.Keys.UnparsableNumber_1, code), e); } } /** * Creates a grid geometry from the current row in the specified result set. * * @param results The result set to read. * @param identifier The identifier of the grid geometry to create. * @return The entry for current row in the specified result set. * @throws SQLException if an error occurred while reading the database. */ @Override @SuppressWarnings("fallthrough") protected GridGeometryEntry createEntry(final LocalCache lc, final ResultSet results, final Comparable<?> identifier) throws SQLException { final GridGeometryQuery query = (GridGeometryQuery) super.query; final SpatialDatabase database = (SpatialDatabase) getDatabase(); final int width = results.getInt (indexOf(query.width)); final int height = results.getInt (indexOf(query.height)); final double scaleX = results.getDouble(indexOf(query.scaleX)); final double shearY = results.getDouble(indexOf(query.shearY)); final double shearX = results.getDouble(indexOf(query.shearX)); final double scaleY = results.getDouble(indexOf(query.scaleY)); final double translateX = results.getDouble(indexOf(query.translateX)); final double translateY = results.getDouble(indexOf(query.translateY)); final int horizontalSRID = results.getInt (indexOf(query.horizontalSRID)); final int verticalSRID = results.getInt (indexOf(query.verticalSRID)); final Array verticalOrdinates = results.getArray (indexOf(query.verticalOrdinates)); /* * Creates the SpatialRefSysEntry object, looking for an existing one in the cache first. * If a new object has been created, it will be completed after insertion in the cache. */ SpatialRefSysEntry srsEntry = new SpatialRefSysEntry(horizontalSRID, verticalSRID, database.temporalCRS); synchronized (gridCRS) { final SpatialRefSysEntry candidate = gridCRS.unique(srsEntry); if (candidate != srsEntry) { srsEntry = candidate; } else try { srsEntry.createSpatioTemporalCRS(database); } catch (FactoryException exception) { gridCRS.remove(srsEntry); final Column column; switch (srsEntry.uninitialized()) { case 1: column = query.horizontalSRID; break; case 2: column = query.verticalSRID; break; default: column = query.identifier; break; } throw new IllegalRecordException(exception, this, results, indexOf(column), identifier); } } final double[] altitudes = asDoubleArray(verticalOrdinates); final MathTransformFactory mtFactory = database.getMathTransformFactory(); final AffineTransform2D at = new AffineTransform2D(scaleX, shearY, shearX, scaleY, translateX, translateY); final Dimension size = new Dimension(width, height); final GridGeometryEntry entry; try { entry = new GridGeometryEntry(identifier, size, srsEntry, at, altitudes, mtFactory); } catch (RuntimeException exception) { throw exception; } catch (Exception exception) { // We want to catch only the checked exceptions here. throw new IllegalRecordException(exception, this, results, indexOf(query.identifier), identifier); } if (entry.isEmpty()) { throw new IllegalRecordException(errors().getString(Errors.Keys.EmptyEnvelope2d), this, results, indexOf(width == 0 ? query.width : height == 0 ? query.height : query.identifier), identifier); } return entry; } /** * Returns the specified SQL array as an array of type {@code double[]}, or {@code null} * if the SQL array is null. The array is {@linkplain Array#free freeded} by this method. */ private static double[] asDoubleArray(final Array verticalOrdinates) throws SQLException { final double[] altitudes; if (verticalOrdinates != null) { final Object data = verticalOrdinates.getArray(); final int length = getLength(data); altitudes = new double[length]; final Number[] asNumbers = (data instanceof Number[]) ? (Number[]) data : null; for (int i=0; i<length; i++) { final double z; if (asNumbers != null) { z = asNumbers[i].doubleValue(); } else { z = getDouble(data, i); } altitudes[i] = z; } // TODO: Uncomment when the JDBC driver will support this method. // In order to test if the JDBC driver support this method, just // uncomment the line below and try running GridGeometryTableTest. // verticalOrdinates.free(); } else { altitudes = null; } return altitudes; } /** * Returns {@code true} if the specified arrays are equal when comparing the values * at {@code float} precision. This method is a workaround for the cases where some * original array was stored with {@code double} precision while the other array has * been casted to {@code float} precision. The precision lost causes the comparison * to fail when comparing the array at full {@code double} precision. For example * {@code (double) 0.1f} is not equals to {@code 0.1}. */ private static boolean equalsAsFloat(final double[] a1, final double[] a2) { if (a1 == null || a2 == null || a1.length != a2.length) { return false; } for (int i=0; i<a1.length; i++) { if (Float.floatToIntBits((float) a1[i]) != Float.floatToIntBits((float) a2[i])) { return false; } } return true; } /** * Returns the identifier for the specified grid geometry. * * @param size The image width and height in pixels. * @param gridToCRS The transform from grid coordinates to "real world" coordinates. * @param horizontalSRID The "real world" horizontal coordinate reference system. * @param verticalOrdinates The vertical coordinates, or {@code null}. * @param verticalSRID The "real world" vertical coordinate reference system. * Ignored if {@code verticalOrdinates} is {@code null}. * @return The identifier of a matching entry, or {@code null} if none was found. * @throws SQLException If the operation failed. */ Integer find(final Dimension size, final AffineTransform gridToCRS, final int horizontalSRID, final double[] verticalOrdinates, final int verticalSRID) throws SQLException { ArgumentChecks.ensureNonNull("size", size); ArgumentChecks.ensureNonNull("gridToCRS", gridToCRS); Integer id = null; final GridGeometryQuery query = (GridGeometryQuery) super.query; final LocalCache lc = getLocalCache(); synchronized (lc) { final LocalCache.Stmt ce = getStatement(lc, QueryType.LIST); final PreparedStatement statement = ce.statement; statement.setInt (indexOf(query.byWidth), size.width ); statement.setInt (indexOf(query.byHeight), size.height); statement.setDouble(indexOf(query.byScaleX), gridToCRS.getScaleX()); statement.setDouble(indexOf(query.byShearY), gridToCRS.getShearY()); statement.setDouble(indexOf(query.byShearX), gridToCRS.getShearX()); statement.setDouble(indexOf(query.byScaleY), gridToCRS.getScaleY()); statement.setDouble(indexOf(query.byTranslateX), gridToCRS.getTranslateX()); statement.setDouble(indexOf(query.byTranslateY), gridToCRS.getTranslateY()); statement.setInt (indexOf(query.byHorizontalSRID), horizontalSRID); boolean foundStrictlyEquals = false; final int idIndex = indexOf(query.identifier); int vsIndex = indexOf(query.verticalSRID); int voIndex = indexOf(query.verticalOrdinates); try (ResultSet results = statement.executeQuery()) { while (results.next()) { final int nextID = results.getInt(idIndex); final int nextSRID = results.getInt(vsIndex); /* * We check vertical SRID in Java code rather than in the SQL statement because it is * uneasy to write a statement that works for both non-null and null values (the former * requires "? IS NULL" since the "? = NULL" statement doesn't work with PostgreSQL 8.2. */ if (results.wasNull() != (verticalOrdinates == null)) { // Inconsistent fields. We will ignore this entry. continue; // } if (verticalOrdinates != null && nextSRID != verticalSRID) { // Not the expected SRID. Search for an other entry. continue; } /* * We compare the arrays in this Java code rather than in the SQL statement (in the * WHERE clause) in order to make sure that we are insensitive to the array type * (since we convert to double[] in all cases), and because we need to relax the * tolerance threshold in some cases. */ final double[] altitudes = asDoubleArray(results.getArray(voIndex)); final boolean isStrictlyEquals; if (Arrays.equals(altitudes, verticalOrdinates)) { isStrictlyEquals = true; } else if (equalsAsFloat(altitudes, verticalOrdinates)) { isStrictlyEquals = false; } else { continue; } /* * If there is more than one record with different ID, then there is a choice: * 1) If the new record is more accurate than the previous one, keep the new one. * 2) Otherwise we keep the previous record. A warning will be logged if and only * if the two records are strictly equals. */ if (id != null && id.intValue() != nextID) { if (!isStrictlyEquals) { continue; } if (foundStrictlyEquals) { // Could happen if there is insufficient conditions in the WHERE clause. log("find", errors().getLogRecord(Level.WARNING, Errors.Keys.DuplicatedRecord_1, id)); continue; } } id = nextID; foundStrictlyEquals = isStrictlyEquals; } } release(lc, ce); } return id; } /** * Returns the identifier for the specified grid geometry. If a suitable entry already * exists, its identifier is returned. Otherwise a new entry is created and its identifier * is returned. * * @param size The image width and height in pixels. * @param gridToCRS The transform from grid coordinates to "real world" coordinates. * @param horizontalSRID The "real world" horizontal coordinate reference system. * @param verticalOrdinates The vertical coordinates, or {@code null}. * @param verticalSRID The "real world" vertical coordinate reference system. * Ignored if {@code verticalOrdinates} is {@code null}. * @return The identifier of a matching entry. * @throws SQLException If the operation failed. */ int findOrCreate(final Dimension size, final AffineTransform gridToCRS, final int horizontalSRID, final double[] verticalOrdinates, final int verticalSRID) throws SQLException { ArgumentChecks.ensureStrictlyPositive("horizontalSRID", horizontalSRID); Integer id; final LocalCache lc = getLocalCache(); synchronized (lc) { boolean success = false; transactionBegin(lc); try { id = find(size, gridToCRS, horizontalSRID, verticalOrdinates, verticalSRID); if (id == null) { /* * No match found. Add a new record in the database. */ final GridGeometryQuery query = (GridGeometryQuery) super.query; final LocalCache.Stmt ce = getStatement(lc, QueryType.INSERT); final PreparedStatement statement = ce.statement; statement.setInt (indexOf(query.width), size.width ); statement.setInt (indexOf(query.height), size.height); statement.setDouble(indexOf(query.scaleX), gridToCRS.getScaleX()); statement.setDouble(indexOf(query.scaleY), gridToCRS.getScaleY()); statement.setDouble(indexOf(query.translateX), gridToCRS.getTranslateX()); statement.setDouble(indexOf(query.translateY), gridToCRS.getTranslateY()); statement.setDouble(indexOf(query.shearX), gridToCRS.getShearX()); statement.setDouble(indexOf(query.shearY), gridToCRS.getShearY()); statement.setInt (indexOf(query.horizontalSRID), horizontalSRID); final int vsIndex = indexOf(query.verticalSRID); final int voIndex = indexOf(query.verticalOrdinates); if (verticalOrdinates == null || verticalOrdinates.length == 0) { statement.setNull(vsIndex, Types.INTEGER); statement.setNull(voIndex, Types.ARRAY); } else { statement.setInt(vsIndex, verticalSRID); final Double[] numbers = new Double[verticalOrdinates.length]; for (int i=0; i<numbers.length; i++) { numbers[i] = Double.valueOf(verticalOrdinates[i]); } final Array array = statement.getConnection().createArrayOf("float8", numbers); statement.setArray(voIndex, array); } success = updateSingleton(statement); /* * Get the identifier of the entry that we just generated. */ try (ResultSet keys = statement.getGeneratedKeys()) { while (keys.next()) { id = keys.getInt(query.identifier.name); if (!keys.wasNull()) break; id = null; // Should never reach this point, but I'm paranoiac. } } release(lc, ce); } } finally { transactionEnd(lc, success); } } if (id == null) { // Should never occur, but I'm paranoiac. throw new SQLNonTransientException(); } return id; } }