This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 8ee6f79 Make the `Gridcoverage` available as a `Feature` property. With this commit, PostGIS rasters are now available through the Feature API. 8ee6f79 is described below commit 8ee6f794b2d3bc7467deaa8b4d46834a50387645 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Dec 25 21:56:38 2021 +0100 Make the `Gridcoverage` available as a `Feature` property. With this commit, PostGIS rasters are now available through the Feature API. --- .../apache/sis/internal/sql/feature/Column.java | 36 ++++--- .../apache/sis/internal/sql/feature/Database.java | 22 +++- .../sis/internal/sql/feature/FeatureAnalyzer.java | 19 +++- .../sis/internal/sql/feature/GeometryGetter.java | 18 +--- .../sis/internal/sql/feature/InfoStatements.java | 115 +++++++++++++-------- .../sis/internal/sql/feature/QueryAnalyzer.java | 2 +- .../org/apache/sis/internal/sql/feature/Table.java | 7 ++ .../sis/internal/sql/feature/TableAnalyzer.java | 4 +- .../sis/internal/sql/feature/ValueGetter.java | 16 --- .../sis/internal/sql/postgis/ExtendedInfo.java | 36 +++++-- .../apache/sis/internal/sql/postgis/Postgres.java | 3 + .../sis/internal/sql/postgis/RasterGetter.java | 90 ++++++++++++++++ .../sis/internal/sql/postgis/RasterReader.java | 38 ++++++- .../java/org/apache/sis/storage/sql/SQLStore.java | 3 + .../sis/internal/sql/postgis/PostgresTest.java | 12 ++- .../sis/internal/sql/postgis/RasterReaderTest.java | 13 ++- 16 files changed, 324 insertions(+), 110 deletions(-) diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java index cd28d13..480b10c 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java @@ -110,10 +110,12 @@ public final class Column { private GeometryType geometryType; /** - * If this column is a geometry column, the Coordinate Reference System (CRS). Otherwise {@code null}. + * If this column is a geometry or raster column, the Coordinate Reference System (CRS). Otherwise {@code null}. * This is determined from the geometry Spatial Reference Identifier (SRID). + * + * @see #getDefaultCRS() */ - private CoordinateReferenceSystem geometryCRS; + private CoordinateReferenceSystem defaultCRS; /** * Converter from {@link ResultSet} column value to value stored in the feature instance. @@ -161,24 +163,26 @@ public final class Column { } /** - * Modifies this column for declaring it as a geometry column. + * Modifies this column for declaring it as a geometry or raster column. * This method is invoked during inspection of the {@code "GEOMETRY_COLUMNS"} table of a spatial database. + * It can also be invoked during the inspection of {@code "GEOGRAPHY_COLUMNS"} or {@code "RASTER_COLUMNS"} + * tables, which are PostGIS extensions. In the raster case, the geometry {@code type} argument shall be null. * * @param caller provider of the locale for error message, if any. - * @param type the type of values in the column, or {@code null} if unknown. + * @param type the type of values in the column, or {@code null} if not geometric. * @param crs the Coordinate Reference System (CRS), or {@code null} if unknown. */ - final void setGeometryInfo(final Localized caller, final GeometryType type, final CoordinateReferenceSystem crs) + final void makeSpatial(final Localized caller, final GeometryType type, final CoordinateReferenceSystem crs) throws DataStoreContentException { final String property; if (geometryType != null && !geometryType.equals(type)) { property = "geometryType"; - } else if (geometryCRS != null && !geometryCRS.equals(crs)) { - property = "geometryCRS"; + } else if (defaultCRS != null && !defaultCRS.equals(crs)) { + property = "defaultCRS"; } else { geometryType = type; - geometryCRS = crs; + defaultCRS = crs; return; } throw new DataStoreContentException(Errors.getResources(caller.getLocale()) @@ -187,22 +191,24 @@ public final class Column { /** * If this column is a geometry column, returns the type of the geometry objects. - * Otherwise returns {@code null}. + * Otherwise returns {@code null} (including the case where this is a raster column). + * Note that if this column is a geometry column but the geometry type was not defined, + * then {@link GeometryType#GEOMETRY} is returned as a fallback. * - * @return type of geometry objects, or {@code null} if unknown or not applicable. + * @return type of geometry objects, or {@code null} if this column is not a geometry column. */ public final GeometryType getGeometryType() { return geometryType; } /** - * If this column is a geometry column, returns the coordinate reference system. + * If this column is a geometry or raster column, returns the default coordinate reference system. * Otherwise returns {@code null}. The CRS may also be null even for a geometry column if it is unspecified. * - * @return CRS of geometries in this column, or {@code null} if unknown or not applicable. + * @return CRS of geometries or rasters in this column, or {@code null} if unknown or not applicable. */ - public final CoordinateReferenceSystem getGeometryCRS() { - return geometryCRS; + public final CoordinateReferenceSystem getDefaultCRS() { + return defaultCRS; } /** @@ -221,7 +227,7 @@ public final class Column { if (isNullable) { attribute.setMinimumOccurs(0); } - valueGetter.getDefaultCRS().ifPresent(attribute::setCRS); + attribute.setCRS(defaultCRS); return attribute; } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java index 7da6ed9..6944bd7 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java @@ -155,6 +155,14 @@ public class Database<G> extends Syntax { private boolean hasGeometry; /** + * {@code true} if this database contains at least one raster column. + * This field is initialized by {@link #analyze analyze(…)} and shall not be modified after that point. + * + * @see #hasRaster() + */ + private boolean hasRaster; + + /** * Catalog and schema of the {@value InfoStatements#GEOMETRY_COLUMNS} and * {@value InfoStatements#SPATIAL_REF_SYS} tables, or null or empty string if none. */ @@ -366,6 +374,7 @@ public class Database<G> extends Syntax { for (final Table table : analyzer.finish()) { tablesByNames.add(store, table.featureType.getName(), table); hasGeometry |= table.hasGeometry; + hasRaster |= table.hasRaster; } tables = tableList.toArray(new Table[tableList.size()]); } @@ -480,6 +489,7 @@ public class Database<G> extends Syntax { /** * Returns {@code true} if this database contains at least one geometry column. + * This information can be used for metadata purpose. * * @return whether at least one geometry column has been found. */ @@ -488,6 +498,16 @@ public class Database<G> extends Syntax { } /** + * Returns {@code true} if this database contains at least one raster column. + * This information can be used for metadata purpose. + * + * @return whether at least one raster column has been found. + */ + public final boolean hasRaster() { + return hasRaster; + } + + /** * Returns a function for getting values from a column having the given definition. * The given definition should include data SQL type and type name. * If no match is found, then this method returns {@code null}. @@ -563,7 +583,7 @@ public class Database<G> extends Syntax { protected final ValueGetter<?> forGeometry(final Column columnDefinition) { final GeometryType type = columnDefinition.getGeometryType(); final Class<? extends G> geometryClass = geomLibrary.getGeometryClass(type).asSubclass(geomLibrary.rootClass); - return new GeometryGetter<>(geomLibrary, geometryClass, columnDefinition.getGeometryCRS(), getBinaryEncoding(columnDefinition)); + return new GeometryGetter<>(geomLibrary, geometryClass, columnDefinition.getDefaultCRS(), getBinaryEncoding(columnDefinition)); } /** diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java index 7afcc4a..6da5908 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.sql.SQLException; import org.opengis.util.GenericName; +import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.feature.builder.FeatureTypeBuilder; @@ -126,6 +127,13 @@ abstract class FeatureAnalyzer { boolean hasGeometry; /** + * Whether this table or view contains at least one raster column. + * + * @see Database#hasRaster + */ + boolean hasRaster; + + /** * Creates a new analyzer. * * @param id the catalog, schema and table name of the table to analyze. @@ -228,11 +236,12 @@ abstract class FeatureAnalyzer { * If geometry columns are found, the first one will be defined as the default geometry. * Note: a future version may allow user to select which column should be the default. */ - if (Geometries.isKnownType(getter.valueType)) { - if (!hasGeometry) { - hasGeometry = true; - attribute.addRole(AttributeRole.DEFAULT_GEOMETRY); - } + if (!hasGeometry && Geometries.isKnownType(getter.valueType)) { + hasGeometry = true; + attribute.addRole(AttributeRole.DEFAULT_GEOMETRY); + } + if (!hasRaster) { + hasRaster = GridCoverage.class.isAssignableFrom(getter.valueType); } } /* diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java index 5d6f2c6..e3622ce 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java @@ -16,7 +16,6 @@ */ package org.apache.sis.internal.sql.feature; -import java.util.Optional; import java.util.OptionalInt; import java.nio.ByteBuffer; import java.sql.ResultSet; @@ -52,7 +51,10 @@ import org.apache.sis.internal.feature.Geometries; * @param <V> the type of geometry objects returned by this getter. * * @version 1.2 - * @since 1.1 + * + * @see org.apache.sis.internal.sql.postgis.RasterGetter + * + * @since 1.1 * @module */ final class GeometryGetter<G, V extends G> extends ValueGetter<V> { @@ -74,13 +76,12 @@ final class GeometryGetter<G, V extends G> extends ValueGetter<V> { /** * Creates a new reader. The same instance can be reused for parsing an arbitrary - * amount of geometries sharing the same CRS. + * amount of geometries sharing the same default CRS. * * @param geometryFactory the factory to use for creating geometries from WKB definitions. * @param geometryClass the type of geometry to be returned by this {@code ValueGetter}. * @param defaultCRS the CRS to use if none can be mapped from the SRID, or {@code null} if none. * @param encoding the way binary data are encoded in the geometry column. - * @return a WKB reader resolving SRID with the specified mapper and default CRS. */ GeometryGetter(final Geometries<G> geometryFactory, final Class<V> geometryClass, final CoordinateReferenceSystem defaultCRS, final BinaryEncoding encoding) @@ -92,15 +93,6 @@ final class GeometryGetter<G, V extends G> extends ValueGetter<V> { } /** - * Returns the default coordinate reference system for this column. - * The default CRS is declared in the {@code "GEOMETRY_COLUMNS"} table. - */ - @Override - public Optional<CoordinateReferenceSystem> getDefaultCRS() { - return Optional.ofNullable(defaultCRS); - } - - /** * Gets the value in the column at specified index. * The given result set must have its cursor position on the line to read. * This method does not modify the cursor position. diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java index 2ec5cf5..9ade14d 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java @@ -89,16 +89,38 @@ public class InfoStatements implements Localized, AutoCloseable { static final String GEOMETRY_COLUMNS = "GEOMETRY_COLUMNS"; /** - * Parameter value for telling that {@code "GEOMETRY_TYPE"} column is expected to contain an integer value. - * This is the encoding used in OGC standard. + * Specifies how the geometry type is encoded in the {@code "GEOMETRY_TYPE"} column. + * The OGC standard defines numeric values, but PostGIS uses textual values. + * + * @see #configureSpatialColumns(PreparedStatement, TableReference, Map, GeometryTypeEncoding) */ - protected static final int COLUMN_TYPE_IS_NUMERIC = 1; + protected enum GeometryTypeEncoding { + /** + * {@code "GEOMETRY_TYPE"} column is expected to contain an integer value. + * This is the encoding used in OGC standard. + */ + NUMERIC, - /** - * Parameter value for telling that {@code "GEOMETRY_TYPE"} column is expected to contain a textual value. - * This is the encoding used by PostGIS, but naming the column as {@code "TYPE"} for avoiding confusion. - */ - protected static final int COLUMN_TYPE_IS_TEXTUAL = 2; + /** + * {@code "GEOMETRY_TYPE"} column is expected to contain a textual value. + * This is the encoding used by PostGIS, but using a different column name + * ({@code "TYPE"} instead of {@code "GEOMETRY_TYPE"}) for avoiding confusion. + */ + TEXTUAL() { + @Override GeometryType parse(final ResultSet result, final int columnIndex) throws SQLException { + return GeometryType.forName(result.getString(columnIndex)); + } + }; + + /** + * Decodes the geometry type encoded in the specified column of the given result set. + * If there is no type information, then this method returns {@code null}. + */ + GeometryType parse(final ResultSet result, final int columnIndex) throws SQLException { + final int code = result.getInt(columnIndex); + return result.wasNull() ? null : GeometryType.forBinaryType(code); + } + } /** * The database that created this set of cached statements. This object includes the @@ -173,29 +195,43 @@ public class InfoStatements implements Localized, AutoCloseable { } /** - * Prepares the statement for fetching information about all geometry columns in a specified table. - * This method is for {@link #completeGeometryColumns(TableReference, Map)} implementations. + * Appends a statement after {@code "WHERE"} such as {@code ""F_TABLE_NAME = ?"}. + * + * @param sql the builder where to add the SQL statement. + * @param prefix the column name prefix: {@code 'F'} for features or {@code 'R'} for rasters. + * @param column the column name (e.g. {@code "TABLE_NAME"}. + * @return the given SQL builder. + */ + private static SQLBuilder appendCondition(final SQLBuilder sql, final char prefix, final String column) { + return sql.append(prefix).append('_').append(column).append(" = ?"); + } + + /** + * Prepares the statement for fetching information about all geometry or raster columns in a specified table. + * This method is for {@link #completeIntrospection(TableReference, Map)} implementations. * - * @param table name of the geometry table. Standard value is {@code "GEOMETRY_COLUMNS"}. - * @param column name of the geometry column. Standard value is {@code "F_GEOMETRY_COLUMN"}. - * @param type name of the type column. Standard value is {@code "GEOMETRY_TYPE"}. + * @param table name of the geometry table. Standard value is {@code "GEOMETRY_COLUMNS"}. + * @param prefix column name prefix: {@code 'F'} for features or {@code 'R'} for rasters. + * @param column name of the geometry column without prefix. Standard value is {@code "GEOMETRY_COLUMN"}. + * @param otherColumn additional columns or {@code null} if none. Standard value is {@code "GEOMETRY_TYPE"}. * @return the prepared statement for querying the geometry table. * @throws SQLException if the statement can not be created. */ - protected final PreparedStatement prepareGeometryStatement(final String table, final String column, final String type) - throws SQLException + protected final PreparedStatement prepareIntrospectionStatement(final String table, + final char prefix, final String column, final String otherColumn) throws SQLException { final SQLBuilder sql = new SQLBuilder(database).append(SQLBuilder.SELECT) - .append(column).append(", ").append(type).append(", SRID "); + .append(prefix).append('_').append(column).append(", SRID "); + if (otherColumn != null) sql.append(", ").append(otherColumn); appendFrom(sql, table); - if (database.supportsCatalogs) sql.append("F_TABLE_CATALOG = ? AND "); - if (database.supportsSchemas) sql.append("F_TABLE_SCHEMA = ? AND "); - sql.append("F_TABLE_NAME = ?"); + if (database.supportsCatalogs) appendCondition(sql, prefix, "TABLE_CATALOG").append(" AND "); + if (database.supportsSchemas) appendCondition(sql, prefix, "TABLE_SCHEMA" ).append(" AND "); + appendCondition(sql, prefix, "TABLE_NAME"); return connection.prepareStatement(sql.toString()); } /** - * Gets all geometry columns for the given table and sets the geometry information on the corresponding columns. + * Gets all geometry and raster columns for the given table and sets information on the corresponding columns. * Column instances in the {@code columns} map are modified in-place (the map itself is not modified). * This method should be invoked before the {@link Column#valueGetter} field is set. * @@ -205,21 +241,23 @@ public class InfoStatements implements Localized, AutoCloseable { * @throws ParseException if the WKT can not be parsed. * @throws SQLException if a SQL error occurred. */ - public void completeGeometryColumns(final TableReference source, final Map<String,Column> columns) throws Exception { + public void completeIntrospection(final TableReference source, final Map<String,Column> columns) throws Exception { if (geometryColumns == null) { - geometryColumns = prepareGeometryStatement(GEOMETRY_COLUMNS, "F_GEOMETRY_COLUMN", "GEOMETRY_TYPE"); + geometryColumns = prepareIntrospectionStatement(GEOMETRY_COLUMNS, 'F', "GEOMETRY_COLUMN", "GEOMETRY_TYPE"); } - completeGeometryColumns(geometryColumns, source, columns, COLUMN_TYPE_IS_NUMERIC); + configureSpatialColumns(geometryColumns, source, columns, GeometryTypeEncoding.NUMERIC); } /** - * Implementation of {@link #completeGeometryColumns(TableReference, Map)}, as a separated methods - * for allowing sub-classes to override above-cited method. + * Implementation of {@link #completeIntrospection(TableReference, Map)} for geometries, + * as a separated methods for allowing sub-classes to override above-cited method. + * May also be used for non-geometric columns such as rasters, in which case the + * {@code typeValueKind} argument shall be {@code null}. * - * @param columnQuery a statement prepared by {@link #prepareGeometryStatement(String, String, String)}. + * @param columnQuery a statement prepared by {@link #prepareIntrospectionStatement(String, char, String, String)}. * @param source the table for which to get all geometry columns. * @param columns all columns for the specified table. Keys are column names. - * @param typeValueKind {@link #COLUMN_TYPE_IS_NUMERIC}, {@link #COLUMN_TYPE_IS_TEXTUAL} or 0 if none. + * @param typeValueKind {@code NUMERIC}, {@code TEXTUAL} or {@code null} if none. * @throws DataStoreContentException if a logical error occurred in processing data. * @throws ParseException if the WKT can not be parsed. * @throws SQLException if a SQL error occurred. @@ -229,8 +267,8 @@ public class InfoStatements implements Localized, AutoCloseable { * unless user has statically defined its column to match a specific geometry type/SRID. * Source: https://gis.stackexchange.com/a/376947/182809 */ - protected final void completeGeometryColumns(final PreparedStatement columnQuery, final TableReference source, - final Map<String,Column> columns, final int typeValueKind) throws Exception + protected final void configureSpatialColumns(final PreparedStatement columnQuery, final TableReference source, + final Map<String,Column> columns, final GeometryTypeEncoding typeValueKind) throws Exception { int p = 0; if (database.supportsCatalogs) columnQuery.setString(++p, source.catalog); @@ -240,22 +278,15 @@ public class InfoStatements implements Localized, AutoCloseable { while (result.next()) { final Column target = columns.get(result.getString(1)); if (target != null) { + final CoordinateReferenceSystem crs = fetchCRS(result.getInt(2)); GeometryType type = null; - switch (typeValueKind) { - case COLUMN_TYPE_IS_TEXTUAL: { - type = GeometryType.forName(result.getString(2)); - break; - } - case COLUMN_TYPE_IS_NUMERIC: { - final int code = result.getInt(2); - if (!result.wasNull()) { - type = GeometryType.forBinaryType(code); - } - break; + if (typeValueKind != null) { + type = typeValueKind.parse(result, 3); + if (type == null) { + type = GeometryType.GEOMETRY; } } - final CoordinateReferenceSystem crs = fetchCRS(result.getInt(3)); - target.setGeometryInfo(this, type, crs); + target.makeSpatial(this, type, crs); } } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java index 164acd4..ec638ea 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java @@ -168,7 +168,7 @@ final class QueryAnalyzer extends FeatureAnalyzer { final InfoStatements spatialInformation = analyzer.spatialInformation; if (spatialInformation != null) { for (final Map.Entry<TableReference, Map<String,Column>> entry : columnsPerTable.entrySet()) { - spatialInformation.completeGeometryColumns(entry.getKey(), entry.getValue()); + spatialInformation.completeIntrospection(entry.getKey(), entry.getValue()); } } /* diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java index a0f71a5..836d92e 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java @@ -125,6 +125,11 @@ final class Table extends AbstractFeatureSet { final boolean hasGeometry; /** + * {@code true} if this table contains at least one raster column. + */ + final boolean hasRaster; + + /** * Map from attribute name to columns. This is built from {@link #columns} array when first needed. * * @see #getColumn(String) @@ -169,6 +174,7 @@ final class Table extends AbstractFeatureSet { primaryKey = analyzer.createAssociations(exportedKeys); // Must be after `spec.createAttributes(…)`. featureType = analyzer.buildFeatureType(); hasGeometry = analyzer.hasGeometry; + hasRaster = analyzer.hasRaster; } /** @@ -191,6 +197,7 @@ final class Table extends AbstractFeatureSet { exportedKeys = parent.exportedKeys; featureType = parent.featureType; hasGeometry = parent.hasGeometry; + hasRaster = parent.hasRaster; } /** diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java index 9f76123..8b2b415 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java @@ -153,7 +153,7 @@ final class TableAnalyzer extends FeatureAnalyzer { @Override final Column[] createAttributes() throws Exception { /* - * Get all columns in advance because `completeGeometryColumns(…)` + * Get all columns in advance because `completeIntrospection(…)` * needs to be invoked before to invoke `database.getMapping(column)`. */ final Map<String,Column> columns = new LinkedHashMap<>(); @@ -167,7 +167,7 @@ final class TableAnalyzer extends FeatureAnalyzer { } final InfoStatements spatialInformation = analyzer.spatialInformation; if (spatialInformation != null) { - spatialInformation.completeGeometryColumns(id, columns); + spatialInformation.completeIntrospection(id, columns); } /* * Analyze the type of each column, which may be geometric as a consequence of above call. diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java index ad0adfc..99ae321 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java @@ -16,7 +16,6 @@ */ package org.apache.sis.internal.sql.feature; -import java.util.Optional; import java.util.Calendar; import java.sql.ResultSet; import java.sql.SQLException; @@ -29,7 +28,6 @@ import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.ZoneOffset; import java.math.BigDecimal; -import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.util.ArgumentChecks; @@ -90,20 +88,6 @@ public abstract class ValueGetter<T> { public abstract T getValue(InfoStatements stmts, ResultSet source, int columnIndex) throws Exception; /** - * Returns the default coordinate reference system for this column. - * The default CRS is declared in the {@code "GEOMETRY_COLUMNS"} table. - * - * <div class="note"><b>Note:</b> - * this method could be used not only for geometric fields, but also on numeric ones representing 1D systems. - * </div> - * - * @return the default coordinate reference system for values in this column. - */ - public Optional<CoordinateReferenceSystem> getDefaultCRS() { - return Optional.empty(); - } - - /** * A getter of {@link Object} values from the current row of a {@link ResultSet}. * This getter delegates to {@link ResultSet#getObject(int)} and returns that value with no change. */ diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java index 398d7d2..e1bbe63d 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java @@ -31,7 +31,7 @@ import org.apache.sis.internal.sql.feature.InfoStatements; * * @author Alexis Manin (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @since 1.1 + * @since 1.2 * @version 1.1 * @module */ @@ -43,6 +43,16 @@ final class ExtendedInfo extends InfoStatements { private PreparedStatement geographyColumns; /** + * A statement for fetching raster information for a specific column. + */ + protected PreparedStatement rasterColumns; + + /** + * The object for reading a raster, or {@code null} if not yet created. + */ + private RasterReader rasterReader; + + /** * Creates an initially empty {@code PostgisStatements} which will use * the given connection for creating {@link PreparedStatement}s. */ @@ -57,15 +67,29 @@ final class ExtendedInfo extends InfoStatements { * @param columns all columns for the specified table. Keys are column names. */ @Override - public void completeGeometryColumns(final TableReference source, final Map<String,Column> columns) throws Exception { + public void completeIntrospection(final TableReference source, final Map<String,Column> columns) throws Exception { if (geometryColumns == null) { - geometryColumns = prepareGeometryStatement("geometry_columns", "f_geometry_column", "type"); + geometryColumns = prepareIntrospectionStatement("geometry_columns", 'f', "geometry_column", "type"); } if (geographyColumns == null) { - geographyColumns = prepareGeometryStatement("geography_columns", "f_geography_column", "type"); + geographyColumns = prepareIntrospectionStatement("geography_columns", 'f', "geography_column", "type"); + } + if (rasterColumns == null) { + rasterColumns = prepareIntrospectionStatement("raster_columns", 'r', "raster_column", null); + } + configureSpatialColumns(geometryColumns, source, columns, GeometryTypeEncoding.TEXTUAL); + configureSpatialColumns(geographyColumns, source, columns, GeometryTypeEncoding.TEXTUAL); + configureSpatialColumns(rasterColumns, source, columns, null); + } + + /** + * Returns a reader for decoding PostGIS Raster binary format to grid coverage instances. + */ + final RasterReader getRasterReader() { + if (rasterReader == null) { + rasterReader = new RasterReader(this); } - completeGeometryColumns(geometryColumns, source, columns, COLUMN_TYPE_IS_TEXTUAL); - completeGeometryColumns(geographyColumns, source, columns, COLUMN_TYPE_IS_TEXTUAL); + return rasterReader; } /** diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java index 86f7f77..87122c2 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java @@ -116,6 +116,9 @@ public final class Postgres<G> extends Database<G> { if ("geography".equalsIgnoreCase(columnDefinition.typeName)) { return forGeometry(columnDefinition); } + if ("raster".equalsIgnoreCase(columnDefinition.typeName)) { + return new RasterGetter(columnDefinition.getDefaultCRS(), getBinaryEncoding(columnDefinition)); + } return super.getMapping(columnDefinition); } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterGetter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterGetter.java new file mode 100644 index 0000000..3912209 --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterGetter.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.sql.postgis; + +import java.sql.ResultSet; +import java.io.InputStream; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.internal.sql.feature.BinaryEncoding; +import org.apache.sis.internal.sql.feature.InfoStatements; +import org.apache.sis.internal.sql.feature.ValueGetter; +import org.apache.sis.coverage.grid.GridCoverage; + + +/** + * Reader of rasters encoded in Well Known Binary (WKB) format. + * At the time of writing this class, raster WKB is a PostGIS-specific format. + * + * <h2>Multi-threading</h2> + * {@code RasterGetter} instances shall be thread-safe. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * + * @see org.apache.sis.internal.sql.feature.GeometryGetter + * + * @since 1.2 + * @module + */ +final class RasterGetter extends ValueGetter<GridCoverage> { + /** + * The Coordinate Reference System if {@link InfoStatements} can not map the SRID. + * This is {@code null} if there is no default. + */ + private final CoordinateReferenceSystem defaultCRS; + + /** + * The way binary data are encoded in the raster column. + */ + private final BinaryEncoding encoding; + + /** + * Creates a new reader. The same instance can be reused for parsing an arbitrary + * amount of rasters sharing the same default CRS. + * + * @param defaultCRS the CRS to use if none can be mapped from the SRID, or {@code null} if none. + * @param encoding the way binary data are encoded in the raster column. + */ + RasterGetter(final CoordinateReferenceSystem defaultCRS, final BinaryEncoding encoding) { + super(GridCoverage.class); + this.defaultCRS = defaultCRS; + this.encoding = encoding; + } + + /** + * Gets the value in the column at specified index. + * The given result set must have its cursor position on the line to read. + * This method does not modify the cursor position. + * + * @param stmts prepared statements for fetching CRS from SRID, or {@code null} if none. + * @param source the result set from which to get the value. + * @param columnIndex index of the column in which to get the value. + * @return raster value in the given column. May be {@code null}. + * @throws Exception if an error occurred. May be an SQL error, a WKB parsing error, <i>etc.</i> + */ + @Override + public GridCoverage getValue(final InfoStatements stmts, final ResultSet source, final int columnIndex) throws Exception { + InputStream stream = source.getBinaryStream(columnIndex); + if (stream != null && stmts instanceof ExtendedInfo) { + stream = encoding.decode(stream); + final RasterReader reader = ((ExtendedInfo) stmts).getRasterReader(); + reader.defaultCRS = defaultCRS; + return reader.readAsCoverage(reader.channel(stream)); + } + return null; + } +} diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java index 693c6ed..82061cd 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java @@ -19,6 +19,8 @@ package org.apache.sis.internal.sql.postgis; import java.util.List; import java.util.Arrays; import java.nio.ByteOrder; +import java.nio.ByteBuffer; +import java.io.InputStream; import java.io.IOException; import java.lang.reflect.Array; import java.awt.image.ColorModel; @@ -35,6 +37,7 @@ import java.awt.image.DataBufferDouble; import java.awt.image.WritableRaster; import java.awt.image.BufferedImage; import java.awt.image.RasterFormatException; +import java.nio.channels.Channels; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridCoverage; @@ -81,6 +84,12 @@ public final class RasterReader extends RasterFormat { private AffineTransform2D gridToCRS; /** + * The default Coordinate Reference System (CRS) if the raster does not specify a CRS. + * This is {@code null} if there is no default. + */ + public CoordinateReferenceSystem defaultCRS; + + /** * The spatial reference identifier, or 0 if undefined. * Note that this is a primary key in the {@code "spatial_ref_sys"} table, not necessarily an EPSG code. */ @@ -98,6 +107,12 @@ public final class RasterReader extends RasterFormat { private transient SampleModel cachedModel; /** + * A temporary buffer for the bytes in the process of being decoded. + * Initially null and created when first needed. + */ + private ByteBuffer buffer; + + /** * Creates a new reader. If the {@code spatialRefSys} argument is non-null, * then this object is valid only as long as the caller holds a connection to the database. * @@ -342,14 +357,15 @@ public final class RasterReader extends RasterFormat { if (image == null) { return null; } - final CoordinateReferenceSystem crs; + CoordinateReferenceSystem crs = null; final int srid = getSRID(); if (spatialRefSys != null) { crs = spatialRefSys.fetchCRS(srid); } else if (srid > 0) { crs = CRS.forCode(Constants.EPSG + ':' + srid); - } else { - crs = null; + } + if (crs == null) { + crs = defaultCRS; } final GridExtent extent = new GridExtent(image.getWidth(), image.getHeight()); final GridGeometry domain = new GridGeometry(extent, ANCHOR, getGridToCRS(), crs); @@ -375,4 +391,20 @@ public final class RasterReader extends RasterFormat { } return new GridCoverage2D(domain, range, image); } + + /** + * Wraps the given input stream into a channel that can be used by {@code read(…)} methods in this class. + * The returned channel should be used and discarded before to create a new {@code ChannelDataInput}, + * because this method recycles the same {@link ByteBuffer}. + * + * @param input the input stream to wrap. + * @return a channel together with a buffer. + * @throws IOException if an error occurred while reading data from the input stream. + */ + public ChannelDataInput channel(final InputStream input) throws IOException { + if (buffer == null) { + buffer = ByteBuffer.allocate(8192); + } + return new ChannelDataInput("raster", Channels.newChannel(input), buffer, false); + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java index 5a0f9be..7d2dd94 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java @@ -248,6 +248,9 @@ public class SQLStore extends DataStore implements Aggregate { if (model.hasGeometry()) { builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR); } + if (model.hasRaster()) { + builder.addSpatialRepresentation(SpatialRepresentationType.GRID); + } model.listTables(c.getMetaData(), builder); } catch (DataStoreException e) { throw e; diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java index 7b67836..63af275 100644 --- a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java +++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java @@ -33,11 +33,13 @@ import org.apache.sis.storage.sql.SQLStoreProvider; import org.apache.sis.storage.sql.ResourceDefinition; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.sql.SQLStoreTest; +import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.internal.storage.io.ChannelDataInput; import org.apache.sis.internal.sql.feature.BinaryEncoding; import org.apache.sis.internal.sql.feature.GeometryGetterTest; import org.apache.sis.internal.feature.jts.JTS; import org.apache.sis.referencing.CRS; +import org.apache.sis.referencing.CommonCRS; import org.apache.sis.referencing.crs.HardCodedCRS; import org.apache.sis.test.sql.TestDatabase; import org.apache.sis.test.DependsOn; @@ -163,12 +165,15 @@ public final strictfp class PostgresTest extends TestCase { * This method performs only a superficial verification of geometries. */ private static void validate(final Feature feature) { - final String filename = feature.getPropertyValue("filename").toString(); - final Geometry geometry = (Geometry) feature.getPropertyValue("geometry"); - final int geomSRID; + final String filename = feature.getPropertyValue("filename").toString(); + final Geometry geometry = (Geometry) feature.getPropertyValue("geometry"); + final GridCoverage raster = (GridCoverage) feature.getPropertyValue("image"); + final int geomSRID; switch (filename) { case "raster-ushort.wkb": { assertNull(geometry); + RasterReaderTest.compareReadResult(TestRaster.USHORT, raster); + assertSame(CommonCRS.WGS84.normalizedGeographic(), raster.getCoordinateReferenceSystem()); return; } case "point-prj": { @@ -190,5 +195,6 @@ public final strictfp class PostgresTest extends TestCase { } catch (FactoryException e) { throw new AssertionError(e); } + assertNull(raster); } } diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java index fc7595a..619e526 100644 --- a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java +++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java @@ -46,14 +46,14 @@ public final strictfp class RasterReaderTest extends TestCase { */ @Test public void testUShort() throws Exception { - compareReadResult(TestRaster.USHORT); + RasterReaderTest.compareReadResult(TestRaster.USHORT); } /** * Reads the file for the given test enumeration and compares with the expected raster. */ private static void compareReadResult(final TestRaster test) throws Exception { - compareReadResult(test, new RasterReader(null), test.input()); + RasterReaderTest.compareReadResult(test, new RasterReader(null), test.input()); } /** @@ -63,9 +63,16 @@ public final strictfp class RasterReaderTest extends TestCase { static void compareReadResult(final TestRaster test, final RasterReader reader, final ChannelDataInput input) throws Exception { final GridCoverage coverage = reader.readAsCoverage(input); input.channel.close(); - final RenderedImage image = coverage.render(null); assertEquals(TestRaster.SRID, reader.getSRID()); assertEquals(TestRaster.getGridToCRS(), reader.getGridToCRS()); + compareReadResult(test, coverage); + } + + /** + * Compares the given image with the expected raster. + */ + static void compareReadResult(final TestRaster test, final GridCoverage coverage) { + final RenderedImage image = coverage.render(null); final DataBufferUShort expected = (DataBufferUShort) test.createRaster().getDataBuffer(); final DataBufferUShort actual = (DataBufferUShort) image.getTile(0, 0).getDataBuffer(); assertTrue(Arrays.deepEquals(expected.getBankData(), actual.getBankData()));