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
commit d65b2a0595a2db7a91c98716753ec1b01a49b9f5 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Sep 10 12:24:01 2021 +0200 Provide a common ground between `FeatureQuery` and `CoverageQuery`. --- .../java/org/apache/sis/storage/CoverageQuery.java | 125 +++++++++++++++++++-- .../org/apache/sis/storage/CoverageSubset.java | 9 +- .../java/org/apache/sis/storage/FeatureQuery.java | 44 ++++++++ .../main/java/org/apache/sis/storage/Query.java | 74 +++++++----- .../org/apache/sis/storage/CoverageQueryTest.java | 38 ++++++- .../org/apache/sis/storage/FeatureQueryTest.java | 19 +++- 6 files changed, 263 insertions(+), 46 deletions(-) diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java index c668af6..c188cf1 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java @@ -16,23 +16,40 @@ */ package org.apache.sis.storage; +import java.util.List; import java.util.Arrays; import java.util.Objects; import java.io.Serializable; import java.math.RoundingMode; +import org.opengis.util.GenericName; +import org.opengis.util.InternationalString; +import org.opengis.geometry.Envelope; import org.opengis.metadata.extent.GeographicBoundingBox; import org.apache.sis.measure.Angle; import org.apache.sis.measure.Latitude; import org.apache.sis.measure.Longitude; import org.apache.sis.measure.AngleFormat; +import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.coverage.grid.GridRoundingMode; import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.resources.Errors; +import org.apache.sis.util.resources.Vocabulary; /** * Definition of filtering to apply for fetching a subset of {@link GridCoverageResource}. * This query allows requesting a subset of the coverage domain and the range. * + * <h2>Terminology</h2> + * This class uses relational database terminology for consistency with generic queries: + * <ul> + * <li>A <cite>selection</cite> is a filter choosing the cells or pixels to include in the subset. + * In this context, the selection is the <cite>coverage domain</cite>.</li> + * <li>A <cite>projection</cite> (not to be confused with map projection) is the set of sample values to keep. + * In this context, the projection is the <cite>coverage range</cite> (i.e. set of sample dimensions).</li> + * </ul> + * * <h2>Optional values</h2> * All aspects of this query are optional and initialized to "none". * Unless otherwise specified, all methods accept a null argument or can return a null value, which means "none". @@ -51,15 +68,23 @@ public class CoverageQuery extends Query implements Cloneable, Serializable { /** * Desired grid extent and resolution, or {@code null} for reading the whole domain. + * This is the "selection" in query terminology. */ private GridGeometry domain; /** * 0-based indices of sample dimensions to read, or {@code null} for reading them all. + * This is the "projection" (not to be confused with map projection) in query terminology. */ private int[] range; /** + * The {@linkplain #range} specified by names instead than indices. + * At most one of {@code range} and {@code rangeNames} shall be non-null. + */ + private String[] rangeNames; + + /** * Number of additional cells to read on each border of the source grid coverage. * If non-zero, this property expands the {@linkplain #domain} to be read by an amount * specified in unit of cells of the image to be read. Those cells do not necessarily @@ -74,26 +99,67 @@ public class CoverageQuery extends Query implements Cloneable, Serializable { } /** - * Sets the desired grid extent and resolution. + * Sets the approximate area of cells or pixels to include in the subset. + * This convenience method creates a grid geometry containing only the given envelope. + * Note that the given envelope is approximate: + * Coverages may expand the envelope to an integer amount of tiles. + * + * @param domain the approximate area of interest, or {@code null} if none. + */ + @Override + public void setSelection(final Envelope domain) { + GridGeometry g = null; + if (domain != null) { + g = new GridGeometry(null, null, domain, GridRoundingMode.NEAREST); + } + setSelection(g); + } + + /** + * Sets the desired grid extent and resolution. The given domain is approximate: + * Coverages may use a different resolution and expand the envelope to an integer amount of tiles. * * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. */ - public void setDomain(final GridGeometry domain) { + public void setSelection(final GridGeometry domain) { this.domain = domain; } /** * Returns the desired grid extent and resolution. - * This is the value set by the last call to {@link #setDomain(GridGeometry)}. + * This is the value set by the last call to {@link #setSelection(GridGeometry)}. + * + * <div class="note"><b>Note on terminology:</b> + * "selection" is the generic term used in queries for designating a subset of feature instances. + * In a grid coverage, feature instances are cells or pixels. + * So this concept maps to the <cite>coverage domain</cite>.</div> * * @return desired grid extent and resolution, or {@code null} for reading the whole domain. */ - public GridGeometry getDomain() { + public GridGeometry getSelection() { return domain; } /** - * Sets the indices of samples dimensions to read. + * Sets the sample dimensions to read by their names. + * + * @param range sample dimensions to retrieve, or {@code null} to retrieve all properties. + * @throws IllegalArgumentException if a sample dimension is duplicated. + */ + @Override + @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") + public void setProjection(String... range) { + if (range != null) { + range = range.clone(); + ArgumentChecks.ensureNonEmpty("range", range); + // Assign only after we verified that the argument is valid. + } + rangeNames = range; + this.range = null; + } + + /** + * Sets the indices of samples dimensions to read (the <cite>coverage range</cite>). * A {@code null} value means to read all sample dimensions (no filtering on range). * If non-null, then the {@code range} array shall contain at least one element, * all elements must be positive and no value can be duplicated. @@ -102,28 +168,61 @@ public class CoverageQuery extends Query implements Cloneable, Serializable { * @throws IllegalArgumentException if the given array is empty or contains negative or duplicated values. */ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") - public void setRange(int... range) { + public void setProjection(int... range) { if (range != null) { range = range.clone(); ArgumentChecks.ensureNonEmpty("range", range, 0, Integer.MAX_VALUE, true); // Assign only after we verified that the argument is valid. } this.range = range; + rangeNames = null; } /** * Returns the indices of samples dimensions to read, or {@code null} if there is no filtering on range. * If non-null, the returned array shall never be empty. * + * <div class="note"><b>Note on terminology:</b> + * "projection" (not to be confused with map projection) is the generic term used in queries + * for designating a subset of feature properties retained in each feature instances. + * In a coverage, this concept maps to the <cite>coverage range</cite>.</div> + * * @return 0-based indices of sample dimensions to read, or {@code null} for reading them all. */ - public int[] getRange() { + public int[] getProjection() { return (range != null) ? range.clone() : null; } /** + * Converts the sample dimension names to sample dimension indices. + * This conversion depends on the resource on which the query will be applied. + * + * @param source the resource for which to to the conversion. + */ + private void namesToIndices(final GridCoverageResource source) throws DataStoreException { + if (rangeNames != null) { + final List<SampleDimension> sd = source.getSampleDimensions(); + final int numBands = sd.size(); + range = new int[rangeNames.length]; +next: for (int i=0; i<rangeNames.length; i++) { + final String name = rangeNames[i]; + for (int j=0; j<numBands; j++) { + if (name.equals(sd.get(j).getName().toString())) { + range[i] = j; + continue next; + } + } + InternationalString id = source.getIdentifier().map(GenericName::toInternationalString) + .orElseGet(() -> Vocabulary.formatInternational(Vocabulary.Keys.Unnamed)); + throw new UnsupportedQueryException(Errors.format(Errors.Keys.PropertyNotFound_2, id, name)); + } + rangeNames = null; + } + } + + /** * Sets a number of additional cells to read on each border of the source grid coverage. - * If non-zero, this property expands the {@linkplain #getDomain() domain} to be read + * If non-zero, this property expands the {@link #getSelection() domain} to be read * by the specified margin. * * <h4>Unit of measurement</h4> @@ -133,7 +232,7 @@ public class CoverageQuery extends Query implements Cloneable, Serializable { * the full image to be read from the resource. Cells are counted after subsampling, * e.g. cells are twice bigger if a subsampling of 2 is applied. * Those cells do not necessarily have the same size than the cells - * of the {@linkplain #getDomain() domain of this query}. + * of the {@link #getSelection() domain of this query}. * * <h4>Use case</h4> * At reading time it may be necessary to add a margin to the coverage extent. @@ -163,11 +262,13 @@ public class CoverageQuery extends Query implements Cloneable, Serializable { * * @param source the coverage resource to filter. * @return a view over the given coverage resource containing only the given domain and range. - * @throws UnsupportedQueryException if this query contains filtering options not yet supported. + * @throws DataStoreException if an error occurred during creation of the subset. */ - final GridCoverageResource execute(final GridCoverageResource source) throws UnsupportedQueryException { + final GridCoverageResource execute(final GridCoverageResource source) throws DataStoreException { ArgumentChecks.ensureNonNull("source", source); - return new CoverageSubset(source, clone()); + final CoverageQuery query = clone(); + query.namesToIndices(source); + return new CoverageSubset(source, query); } /** diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java index 4ff419f..b93ef03 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java @@ -54,6 +54,7 @@ final class CoverageSubset extends AbstractGridResource { /** * Creates a new coverage resource by filtering the given coverage using the given query. + * This given query is stored as-is (it is not cloned neither optimized). * * @param source the coverage resource instances which provides the data. * @param query the domain and range to read from the {@code source} coverage. @@ -99,7 +100,7 @@ final class CoverageSubset extends AbstractGridResource { private GridGeometry clip(final GridGeometry domain, final GridRoundingMode rounding, final GridClippingMode clipping) throws DataStoreException { - final GridGeometry areaOfInterest = query.getDomain(); + final GridGeometry areaOfInterest = query.getSelection(); if (domain == null) return areaOfInterest; if (areaOfInterest == null) return domain; try { @@ -135,7 +136,7 @@ final class CoverageSubset extends AbstractGridResource { @Override public List<SampleDimension> getSampleDimensions() throws DataStoreException { final List<SampleDimension> dimensions = source.getSampleDimensions(); - final int[] range = query.getRange(); + final int[] range = query.getProjection(); if (range == null) { return dimensions; } @@ -155,7 +156,7 @@ final class CoverageSubset extends AbstractGridResource { * Loads a subset of the grid coverage represented by this resource. * The domain to be read by the resource is computed as below: * <ul> - * <li>If the query specifies a {@linkplain CoverageQuery#getDomain() domain}, + * <li>If the query specifies a {@link CoverageQuery#getSelection() domain}, * the given domain is intersected with the query domain.</li> * <li>If the query specifies a {@linkplain CoverageQuery#getSourceDomainExpansion() domain expansion}, * the given domain is expanded by the amount specified in the query.</li> @@ -176,7 +177,7 @@ final class CoverageSubset extends AbstractGridResource { * specified `domain` but inside the source domain, which may be larger. */ domain = clip(domain, GridRoundingMode.ENCLOSING, GridClippingMode.BORDER_EXPANSION); - final int[] qr = query.getRange(); + final int[] qr = query.getProjection(); if (range == null) { range = qr; } else if (qr != null) { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java index 927ec64..c2b4e6c 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java @@ -25,11 +25,14 @@ import java.io.Serializable; import javax.measure.Quantity; import javax.measure.quantity.Length; import org.opengis.util.GenericName; +import org.opengis.geometry.Envelope; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; +import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.internal.feature.FeatureExpression; import org.apache.sis.internal.filter.SortByComparator; import org.apache.sis.internal.storage.Resources; +import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.filter.Optimization; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Classes; @@ -39,6 +42,7 @@ import org.apache.sis.util.iso.Names; // Branch-dependent imports import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; +import org.opengis.filter.FilterFactory; import org.opengis.filter.Filter; import org.opengis.filter.Expression; import org.opengis.filter.Literal; @@ -150,6 +154,29 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { } /** + * Sets the properties to retrieve by their names. This convenience method wraps the + * given names in {@link ValueReference} expressions without alias and delegates to + * {@link #setProjection(NamedExpression...)}. + * + * @param properties properties to retrieve, or {@code null} to retrieve all properties. + * @throws IllegalArgumentException if a property is duplicated. + */ + @Override + public void setProjection(final String... properties) { + NamedExpression[] wrappers = null; + if (properties != null) { + final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); + wrappers = new NamedExpression[properties.length]; + for (int i=0; i<wrappers.length; i++) { + final String p = properties[i]; + ArgumentChecks.ensureNonNullElement("properties", i, p); + wrappers[i] = new NamedExpression(ff.property(p)); + } + } + setProjection(wrappers); + } + + /** * Sets the properties to retrieve, or {@code null} if all properties shall be included in the query. * This convenience method wraps the given expression in {@link NamedExpression}s without alias and * delegates to {@link #setProjection(NamedExpression...)}. @@ -213,6 +240,23 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { } /** + * Sets the approximate area of feature instances to include in the subset. + * This convenience method creates a filter that checks if the bounding box + * of the feature's {@code "sis:geometry"} property interacts with the given envelope. + * + * @param domain the approximate area of interest, or {@code null} if none. + */ + @Override + public void setSelection(final Envelope domain) { + Filter<? super Feature> filter = null; + if (domain != null) { + final FilterFactory<Feature,Object,?> ff = DefaultFilterFactory.forFeatures(); + filter = ff.bbox(ff.property(AttributeConvention.GEOMETRY), domain); + } + setSelection(filter); + } + + /** * Sets a filter for trimming feature instances. * Features that do not pass the filter are discarded. * Discarded features are not counted for the {@linkplain #setLimit(long) query limit}. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java index 2aa4b8f..4d58d2f 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java @@ -16,39 +16,36 @@ */ package org.apache.sis.storage; -import org.opengis.feature.Feature; +import org.opengis.geometry.Envelope; + +// Branch-dependent imports import org.opengis.filter.QueryExpression; /** * Definition of filtering to apply for fetching a resource subset. * Filtering can be applied on {@link FeatureSet} or on {@link GridCoverageResource}. - * When applied on {@link FeatureSet}, filtering can happen in two domains: - * - * <ol> - * <li>By filtering the {@link Feature} instances.</li> - * <li>By filtering the {@linkplain org.apache.sis.feature.DefaultFeatureType#getProperty properties} - * in each feature instance.</li> - * </ol> - * - * Compared to Java functional interfaces, the first domain is equivalent to using - * <code>{@linkplain java.util.function.Predicate}<{@linkplain Feature}></code> - * while the second domain is equivalent to using - * <code>{@linkplain java.util.function.UnaryOperator}<{@linkplain Feature}></code>. + * A query contains at least two parts: * - * <div class="note"><b>Note:</b> - * it is technically possible to use {@code Query} for performing more generic feature transformations, - * for example inserting new properties computed from other properties, but such {@code Query} usages - * should be rare since transformations (or more generic processing) are the topic of another package. - * Queries are rather descriptive objects used by {@link FeatureSet} to optimize search operations - * as much as possible on the resource, using for example caches and indexes.</div> + * <ul> + * <li><b>Selection</b> for choosing the feature instances to fetch. + * This is equivalent to choosing rows in a database table.</li> + * <li><b>Projection</b> (not to be confused with map projection) for choosing the + * {@linkplain org.apache.sis.feature.DefaultFeatureType#getProperty feature properties} or the + * {@linkplain org.apache.sis.coverage.SampleDimension coverage sample dimensions} to fetch. + * This is equivalent to choosing columns in a database table.</li> + * </ul> * * Compared to the SQL language, {@code Query} contains the information in the {@code SELECT} and * {@code WHERE} clauses of a SQL statement. A {@code Query} typically contains filtering capabilities * and (sometime) simple attribute transformations. Well known query languages include SQL and CQL. * + * <h2>Optional values</h2> + * All aspects of this query are optional and initialized to "none". + * Unless otherwise specified, all methods accept a null argument or can return a null value, which means "none". + * * @author Johann Sorel (Geomatys) - * @version 1.0 + * @version 1.1 * * @see FeatureSet#subset(Query) * @see GridCoverageResource#subset(Query) @@ -57,15 +54,40 @@ import org.opengis.filter.QueryExpression; * @module */ public abstract class Query implements QueryExpression { - /* - * Current version does not yet contain any field. But some fields may be added in the future. - * For example some methods from org.apache.sis.internal.storage.query.FeatureQuery may move here. - * We use an abstract class instead than an interface for that reason. - */ - /** * Creates a new, initially empty, query. */ protected Query() { } + + /** + * Sets the approximate area of feature instances or pixels to include in the subset. + * For feature set, the domain is materialized by a {@link org.opengis.filter.Filter}. + * For grid coverage resource, the given envelope specifies the coverage domain. + * + * <p>The given envelope is approximate. + * Features may test intersections using only bounding boxes instead of full geometries. + * Coverages may expand the envelope to an integer amount of tiles.</p> + * + * @param domain the approximate area of interest, or {@code null} if none. + * + * @since 1.1 + */ + public abstract void setSelection(Envelope domain); + + /** + * Sets the properties to retrieve by their names. For features, the arguments are names of + * {@linkplain org.apache.sis.feature.DefaultFeatureType#getProperty feature properties}. + * For coverages, the arguments are names of + * {@linkplain org.apache.sis.coverage.BandedCoverage#getSampleDimensions() sample dimensions}. + * + * <p><b>Note:</b> in this context, the "projection" word come from relational database terminology. + * It is unrelated to <cite>map projection</cite>.</p> + * + * @param properties properties to retrieve, or {@code null} to retrieve all properties. + * @throws IllegalArgumentException if a property is duplicated. + * + * @since 1.1 + */ + public abstract void setProjection(String... properties); } diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java index eef4d96..305908c 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java @@ -99,7 +99,7 @@ public final strictfp class CoverageQueryTest extends TestCase { public void testWithSubgrid() throws DataStoreException { final GridGeometry subGrid = createSubGrid(0); final CoverageQuery query = new CoverageQuery(); - query.setDomain(subGrid); + query.setSelection(subGrid); final GridCoverageResource subset = resource.subset(query); assertEquals(subGrid, subset.getGridGeometry()); @@ -116,7 +116,7 @@ public final strictfp class CoverageQueryTest extends TestCase { final int expansion = 3; final GridGeometry subGrid = createSubGrid(0); final CoverageQuery query = new CoverageQuery(); - query.setDomain(subGrid); + query.setSelection(subGrid); query.setSourceDomainExpansion(expansion); final GridCoverageResource subset = resource.subset(query); @@ -125,6 +125,40 @@ public final strictfp class CoverageQueryTest extends TestCase { } /** + * Tests using only the methods defined in the {@link Query} base class. + * + * @throws DataStoreException if query execution failed. + */ + @Test + public void testQueryMethods() throws DataStoreException { + final GridGeometry subGrid = createSubGrid(0); + final Query query = new CoverageQuery(); + query.setSelection(subGrid.getEnvelope()); + query.setProjection("0"); + + final GridCoverageResource subset = resource.subset(query); + assertEquals(subGrid, subset.getGridGeometry()); + verifyRead(subset, 0); + } + + /** + * Tests using an invalid sample dimension name. + * + * @throws DataStoreException if query execution failed. + */ + @Test + public void testInvalidName() throws DataStoreException { + final Query query = new CoverageQuery(); + query.setProjection("Apple"); + try { + resource.subset(query); + fail("Expected UnsupportedQueryException."); + } catch (UnsupportedQueryException e) { + assertTrue(e.getMessage().contains("Apple")); + } + } + + /** * Verifies that the read operation adds the expected margins. */ private void verifyRead(final GridCoverageResource subset, final int expansion) throws DataStoreException { diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java index cbe7414..6918ac9 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java @@ -152,7 +152,7 @@ public final strictfp class FeatureQueryTest extends TestCase { * @throws DataStoreException if an error occurred while executing the query. */ @Test - public void testFilter() throws DataStoreException { + public void testSelection() throws DataStoreException { final FilterFactory<Feature,?,?> factory = DefaultFilterFactory.forFeatures(); query.setSelection(factory.equal(factory.property("value1", Integer.class), factory.literal(2), true, MatchAction.ALL)); @@ -165,7 +165,7 @@ public final strictfp class FeatureQueryTest extends TestCase { * @throws DataStoreException if an error occurred while executing the query. */ @Test - public void testColumns() throws DataStoreException { + public void testProjection() throws DataStoreException { final FilterFactory<Feature,?,?> factory = DefaultFilterFactory.forFeatures(); query.setProjection(new FeatureQuery.NamedExpression(factory.property("value1", Integer.class), (String) null), new FeatureQuery.NamedExpression(factory.property("value1", Integer.class), "renamed1"), @@ -194,4 +194,19 @@ public final strictfp class FeatureQueryTest extends TestCase { assertEquals(3, result.getPropertyValue("renamed1")); assertEquals("a literal", result.getPropertyValue("computed")); } + + /** + * Verifies the effect of {@link FeatureQuery#setProjection(String[])}. + * + * @throws DataStoreException if an error occurred while executing the query. + */ + @Test + public void testProjectionByNames() throws DataStoreException { + query.setProjection("value2"); + query.setLimit(1); + final FeatureSet fs = query.execute(featureSet); + final Feature result = TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList())); + final PropertyType p = TestUtilities.getSingleton(result.getType().getProperties(true)); + assertEquals("value2", p.getName().toString()); + } }
