You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2019/10/26 15:42:10 UTC

[sis] branch geoapi-4.0 updated: Provide a default implementation for GridCoverageResource.subset(Query). Not yet in public API; pending the gain of more experience.

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 e4606ae  Provide a default implementation for GridCoverageResource.subset(Query). Not yet in public API; pending the gain of more experience.
e4606ae is described below

commit e4606aec14700b542542503acd85fb2d9cb0c6c9
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Oct 26 17:35:28 2019 +0200

    Provide a default implementation for GridCoverageResource.subset(Query).
    Not yet in public API; pending the gain of more experience.
---
 .../apache/sis/coverage/grid/GridDerivation.java   |   2 +
 .../referencing/provider/MapProjection.java        |   2 +-
 .../java/org/apache/sis/measure/AngleFormat.java   |   1 +
 .../apache/sis/internal/netcdf/RasterResource.java |   4 +-
 .../sis/internal/storage/AbstractGridResource.java |  21 +-
 .../org/apache/sis/internal/storage/Resources.java |   5 +
 .../sis/internal/storage/Resources.properties      |   1 +
 .../sis/internal/storage/Resources_fr.properties   |   1 +
 .../apache/sis/internal/storage/package-info.java  |   2 +-
 .../sis/internal/storage/query/CoverageQuery.java  | 245 ++++++++++++++++++---
 .../sis/internal/storage/query/CoverageSubset.java | 135 ++++++++++++
 .../sis/internal/storage/query/SimpleQuery.java    |  26 ++-
 .../sis/internal/storage/query/package-info.java   |   2 +-
 .../apache/sis/storage/GridCoverageResource.java   |  21 +-
 .../main/java/org/apache/sis/storage/Query.java    |   4 +-
 .../java/org/apache/sis/storage/package-info.java  |   2 +-
 16 files changed, 432 insertions(+), 42 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index e19b807..5367538 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -867,6 +867,8 @@ public class GridDerivation {
      * Builds a grid geometry with the configuration specified by the other methods in this {@code GridDerivation} class.
      *
      * @return the modified grid geometry. May be the {@link #base} grid geometry if no change apply.
+     * @throws IllegalGridGeometryException if the grid geometry can not be computed
+     *         because of arguments given to a {@code subgrid(…)} or other methods.
      *
      * @see #getIntersection()
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
index 3c4df75..680c2df 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
@@ -21,13 +21,13 @@ import java.util.HashMap;
 import java.util.NoSuchElementException;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.measure.Unit;
+import org.opengis.util.GenericName;
 import org.opengis.util.FactoryException;
 import org.opengis.util.InternationalString;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.GeneralParameterDescriptor;
-import org.opengis.util.GenericName;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
index 5921d6b..55902fd 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
@@ -812,6 +812,7 @@ public class AngleFormat extends Format implements Localized {
      *
      * @param  resolution  the desired angle resolution, in decimal degrees.
      * @param  allowFieldChanges  whether this method is allowed to change the set of fields (degrees, minutes or seconds).
+     * @throws IllegalArgumentException if the given resolution is NaN or infinite.
      *
      * @since 1.0
      */
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 9786b78..57f366e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -660,8 +660,8 @@ public final class RasterResource extends AbstractGridResource implements Resour
         } catch (DisjointExtentException e) {
             throw new NoSuchDataException(canNotReadFile(), e);
         } catch (RuntimeException e) {                          // Many exceptions thrown by RasterFactory.wrap(…).
-            final Throwable cause = e.getCause();
-            if (cause instanceof TransformException) {
+            final Exception cause = getReferencingCause(e);
+            if (cause != null) {
                 throw new DataStoreReferencingException(canNotReadFile(), cause);
             } else {
                 throw new DataStoreContentException(canNotReadFile(), e);
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
index 0cd9832..a803548 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
@@ -20,7 +20,10 @@ import java.util.Arrays;
 import java.util.Optional;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
@@ -35,7 +38,7 @@ import org.apache.sis.util.ArraysExt;
  * Base class for implementations of {@link GridCoverageResource}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.8
  * @module
  */
@@ -294,4 +297,20 @@ public abstract class AbstractGridResource extends AbstractResource implements G
             return builder;
         }
     }
+
+    /**
+     * If the given exception is caused by a {@link FactoryException} or {@link TransformException},
+     * returns that cause. Otherwise returns {@code null}. This is a convenience method for deciding
+     * if an exception should be rethrown as an {@link DataStoreReferencingException}.
+     *
+     * @param  e  the exception for which to inspect the cause.
+     * @return the cause if it is a referencing problem, or {@code null} otherwise.
+     */
+    protected static Exception getReferencingCause(final RuntimeException e) {
+        final Throwable cause = e.getCause();
+        if (cause instanceof FactoryException || cause instanceof TransformException) {
+            return (Exception) cause;
+        }
+        return null;
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index c062fab..7f4c1df 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -82,6 +82,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotGetCommonMetadata_2 = 39;
 
         /**
+         * Can not intersect the data with specified query.
+         */
+        public static final short CanNotIntersectDataWithQuery = 57;
+
+        /**
          * Can not read the Coordinate Reference System (CRS) Well Known Text (WKT) in “{0}”.
          */
         public static final short CanNotReadCRS_WKT_1 = 37;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index 39a707a..da7c771 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -23,6 +23,7 @@ AmbiguousName_4                   = Name \u201c{3}\u201d is ambiguous because it
 CanNotCreateFolderStore_1         = Can not create resources based on the content of \u201c{0}\u201d directory.
 CanNotDeriveTypeFromFeature_1     = Can not infer the feature type resulting from \u201c{0}\u201d filtering.
 CanNotGetCommonMetadata_2         = Can not get metadata common to \u201c{0}\u201d files. The reason is: {1}
+CanNotIntersectDataWithQuery      = Can not intersect the data with specified query.
 CanNotReadCRS_WKT_1               = Can not read the Coordinate Reference System (CRS) Well Known Text (WKT) in \u201c{0}\u201d.
 CanNotReadDirectory_1             = Can not read \u201c{0}\u201d directory.
 CanNotReadFile_2                  = Can not read \u201c{1}\u201d as a file in the {0} format.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 0294938..7d62c29 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -28,6 +28,7 @@ AmbiguousName_4                   = Le nom \u00ab\u202f{3}\u202f\u00bb est ambig
 CanNotCreateFolderStore_1         = Ne peut pas cr\u00e9er des ressources bas\u00e9es sur le contenu du r\u00e9pertoire \u00ab\u202f{0}\u202f\u00bb.
 CanNotDeriveTypeFromFeature_1     = Ne peut pas d\u00e9terminer le type de donn\u00e9es r\u00e9sultant du filtrage \u00ab\u202f{0}\u202f\u00bb.
 CanNotGetCommonMetadata_2         = Ne peut pas obtenir les m\u00e9ta-donn\u00e9es communes aux fichiers \u00ab\u202f{0}\u202f\u00bb. La raison est\u2008: {1}
+CanNotIntersectDataWithQuery      = Ne peut pas intercepter les donn\u00e9es avec la requ\u00eate qui a \u00e9t\u00e9 sp\u00e9cifi\u00e9e.
 CanNotReadCRS_WKT_1               = Ne peut pas lire le syst\u00e8me de r\u00e9f\u00e9rence spatial dans le \u00ab\u202fWell Known Text\u202f\u00bb (WKT) de \u00ab\u202f{0}\u202f\u00bb.
 CanNotReadDirectory_1             = Ne peut pas lire le r\u00e9pertoire \u00ab\u202f{0}\u202f\u00bb.
 CanNotReadFile_2                  = Ne peut pas lire \u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java
index 94fad8a..b072411 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java
@@ -25,7 +25,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.4
  * @module
  */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/CoverageQuery.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/CoverageQuery.java
index 21b2897..b88a673 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/CoverageQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/CoverageQuery.java
@@ -16,46 +16,67 @@
  */
 package org.apache.sis.internal.storage.query;
 
+import java.util.Arrays;
+import java.util.Objects;
+import java.math.RoundingMode;
+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.grid.GridGeometry;
+import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.Query;
 import org.apache.sis.util.ArgumentChecks;
 
+
 /**
- * Define a simple query configuration for coverage resources.
+ * A simple query configuration for coverage resources for requesting a subset of the domain and the range.
+ * Experimental for now, may move to public API in a future version.
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
-public final class CoverageQuery extends Query {
+public final class CoverageQuery extends Query implements Cloneable {
+    /**
+     * Desired grid extent and resolution, or {@code null} for reading the whole domain.
+     */
+    private GridGeometry domain;
 
-    private final GridGeometry domain;
-    private final int[] range;
-    private final int margin;
+    /**
+     * 0-based indices of sample dimensions to read, or {@code null} for reading them all.
+     */
+    private int[] range;
 
     /**
-     * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
-     * @param  range   0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
+     * 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
+     * have the same size than the cells of <code>{@linkplain #domain}.getExtent()</code>.
      */
-    public CoverageQuery(GridGeometry domain, int... range) {
-        this(domain, 0, range);
+    private int sourceDomainExpansion;
+
+    /**
+     * Creates a new query performing no filtering.
+     */
+    public CoverageQuery() {
     }
 
     /**
+     * Sets the desired grid extent and resolution.
+     *
      * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
-     * @param  margin  reading margin
-     * @param  range   0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
      */
-    public CoverageQuery(GridGeometry domain, int margin, int... range) {
-        ArgumentChecks.ensureNonNull("margin", margin);
+    public void setDomain(final GridGeometry domain) {
         this.domain = domain;
-        this.margin = margin;
-        this.range = range;
     }
 
     /**
-     * Returns desired grid extent and resolution.
+     * Returns the desired grid extent and resolution.
+     * This is the value set by the last call to {@link #setDomain(GridGeometry)}.
      *
      * @return desired grid extent and resolution, or {@code null} for reading the whole domain.
      */
@@ -64,25 +85,195 @@ public final class CoverageQuery extends Query {
     }
 
     /**
-     * Returns indices of samples dimensions to read.
+     * Sets the indices of samples dimensions to read.
+     *
+     * @param  range   0-based indices of sample dimensions to read,
+     *                 or {@code null} or an empty sequence for reading them all.
+     */
+    public void setRange(final int... range) {
+        this.range = (range != null && range.length != 0) ? range.clone() : null;
+    }
+
+    /**
+     * Returns the indices of samples dimensions to read.
      *
-     * @return 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
+     * @return 0-based indices of sample dimensions to read, or {@code null} for reading them all.
      */
     public int[] getRange() {
-        return range;
+        return (range != null) ? range.clone() : null;
     }
 
     /**
-     * At reading time it may be necessary to add a margin to the read extent.
+     * Converts the given range indices from the indices relative to the sub-coverage
+     * to indices relative to the source coverage.
      *
-     * <p>This margin is used knowing later image processing operations will use a window to
-     * iterator over samples. As example Bilinear interpolation uses a 1 pixel
-     * window.</p>
+     * @param  caller   the caller, used for error messages only.
+     * @param  request  the range indices to convert, or {@code null} or empty if none.
+     * @return the range of indices relative to the source coverage.
+     * @throws IllegalArgumentException if an index is invalid.
+     */
+    final int[] subrange(final CoverageSubset caller, final int[] request) {
+        if (range == null) {
+            return request;
+        }
+        if (request == null || request.length == 0) {
+            return range.clone();
+        }
+        final int[] sub = new int[request.length];
+        for (int i=0; i<request.length; i++) {
+            final int j = request[i];
+            if (j >= 0 && j < range.length) {
+                sub[i] = range[j];
+            } else {
+                throw new IllegalArgumentException(caller.invalidRange(j));
+            }
+        }
+        return sub;
+    }
+
+    /**
+     * Returns the number of range indices, or 0 if no range sub-setting has been specified.
+     */
+    final int getRangeCount() {
+        return (range != null) ? range.length : 0;
+    }
+
+    /**
+     * Set 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
+     * by an amount specified in unit of cells of the image to be read. Those cells do not
+     * necessarily have the same size than the cells of <code>domain.getExtent()</code>.
+     *
+     * <p>At reading time it may be necessary to add such margin to the coverage extent.
+     * This margin is used when the user knows that an image processing operation will
+     * need to iterate over a little bit more data than the area of interest.
+     * For example the bilinear interpolation uses a 2×2 pixels window.</p>
+     *
+     * @param  margin  read margin, which must be zero or positive.
+     */
+    public void setSourceDomainExpansion(final int margin) {
+        ArgumentChecks.ensurePositive("margin", margin);
+        sourceDomainExpansion = margin;
+    }
+
+    /**
+     * Returns the number of additional cells to read on each border of the source grid coverage.
+     * This is the value sets by the last call to {@link #setSourceDomainExpansion(int)}.
      *
-     * @return read margin, zero or positive
+     * @return read margin, zero or positive.
      */
-    public int getMargin() {
-        return margin;
+    public int getSourceDomainExpansion() {
+        return sourceDomainExpansion;
     }
 
+    /**
+     * Applies this query on the given coverage.
+     *
+     * @param  source  the coverage resource to filter.
+     * @return a view over the given coverage resource containing only the given domain and range.
+     */
+    public GridCoverageResource execute(final GridCoverageResource source) {
+        ArgumentChecks.ensureNonNull("source", source);
+        return new CoverageSubset(source, clone());
+    }
+
+    /**
+     * Returns a clone of this query.
+     *
+     * @return a clone of this query.
+     */
+    @Override
+    public CoverageQuery clone() {
+        /*
+         * Implementation note: no need to clone the arrays. It is safe to share the same array instances
+         * because this class does not modify them and does not return them directly to the user.
+         */
+        try {
+            return (CoverageQuery) super.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    /**
+     * Returns a hash code value for this query.
+     *
+     * @return a hash value for this query.
+     */
+    @Override
+    public int hashCode() {
+        return 59 * Objects.hashCode(domain) +
+               37 *  Arrays.hashCode(range)  +
+               31 *  sourceDomainExpansion;
+    }
+
+    /**
+     * Compares this query with the given object for equality.
+     *
+     * @param  obj  the object to compare with this query.
+     * @return whether the two objects are equal.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj != null && getClass() == obj.getClass()) {
+            final CoverageQuery other = (CoverageQuery) obj;
+            return sourceDomainExpansion  == other.sourceDomainExpansion &&
+                      Objects.equals(domain, other.domain) &&
+                       Arrays.equals(range,  other.range);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a textual representation looking like an SQL Select query.
+     *
+     * @return textual representation of this query.
+     */
+    @Override
+    public String toString() {
+        final StringBuffer sb = new StringBuffer(80);
+        sb.append("SELECT ");
+        if (range != null) {
+            sb.append("range[");
+            for (int i=0; i<range.length; i++) {
+                if (i != 0) sb.append(", ");
+                sb.append(range[i]);
+            }
+            sb.append(']');
+        } else {
+            sb.append('*');
+        }
+        if (domain != null) {
+            sb.append(" WHERE domain ∩ [");
+            final GeographicBoundingBox box = domain.getGeographicExtent().orElse(null);
+            if (box != null) {
+                final AngleFormat f = new AngleFormat("D°");
+                append(sb, f, new Latitude (box.getSouthBoundLatitude()),
+                              new Latitude (box.getNorthBoundLatitude()));
+                append(sb, f, new Longitude(box.getWestBoundLongitude()),
+                              new Longitude(box.getEastBoundLongitude()));
+            } else {
+                sb.append('?');     // If the domain can not be expressed as a geographic bounding box.
+            };
+            sb.append(']');
+            if (sourceDomainExpansion != 0) {
+                sb.append(" + margin(").append(sourceDomainExpansion).append(')');
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Formats a range of longitude or latitude values in the pseudo-SQL statement.
+     * This is a helper method for {@link #toString()}.
+     */
+    private static void append(final StringBuffer sb, final AngleFormat f, final Angle start, final Angle end) {
+        final double span = Math.abs(end.degrees() - start.degrees()) / 1000;
+        f.setPrecision(span > 0 && span < Double.POSITIVE_INFINITY ? span : 0, false);  // Also filter NaN values.
+        f.setRoundingMode(RoundingMode.DOWN); f.format(start, sb, null);
+        f.setRoundingMode(RoundingMode.UP);   f.format(end,   sb, null);
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/CoverageSubset.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/CoverageSubset.java
new file mode 100644
index 0000000..2765820
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/CoverageSubset.java
@@ -0,0 +1,135 @@
+/*
+ * 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.storage.query;
+
+import java.util.List;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.internal.storage.AbstractGridResource;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+
+
+/**
+ * The result of {@link CoverageQuery#execute(GridCoverageResource)}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class CoverageSubset extends AbstractGridResource {
+    /**
+     * The coverage resource instances to filter.
+     */
+    private final GridCoverageResource source;
+
+    /**
+     * The query for filtering the source coverage.
+     */
+    private final CoverageQuery query;
+
+    /**
+     * Creates a new coverage resource by filtering the given coverage using the given query.
+     */
+    CoverageSubset(final GridCoverageResource source, final CoverageQuery query) {
+        super(source instanceof StoreListeners ? (StoreListeners) source : null);
+        this.source = source;
+        this.query  = query;
+    }
+
+    /**
+     * Returns the valid extent of grid coordinates clipped to the area specified in the query.
+     *
+     * @return extent of grid coordinates clipped to the query.
+     * @throws DataStoreException if an error occurred while reading definitions from the underlying data store,
+     *         or if the grid geometry can not be clipped to the query area.
+     */
+    @Override
+    public GridGeometry getGridGeometry() throws DataStoreException {
+        GridGeometry domain = source.getGridGeometry();
+        final GridGeometry sub = query.getDomain();
+        if (sub != null) try {
+            domain = domain.derive().subgrid(sub).build();
+        } catch (IllegalArgumentException | IllegalStateException e) {
+            final String msg = Resources.forLocale(getLocale()).getString(Resources.Keys.CanNotIntersectDataWithQuery);
+            final Exception cause = getReferencingCause(e);
+            if (cause != null) {
+                throw new DataStoreReferencingException(msg, cause);
+            } else {
+                throw new DataStoreException(msg, e);
+            }
+        }
+        return domain;
+    }
+
+    /**
+     * Returns the ranges of sample values together with the conversion from samples to real values.
+     * The returned list should never be empty.
+     *
+     * @return ranges of sample values together with their mapping to "real values".
+     * @throws DataStoreException if an error occurred while reading definitions from the underlying data store.
+     */
+    @Override
+    public List<SampleDimension> getSampleDimensions() throws DataStoreException {
+        final List<SampleDimension> dimensions = source.getSampleDimensions();
+        if (query.getRangeCount() == 0) {
+            return dimensions;
+        }
+        final int[] range = query.getRange();
+        final SampleDimension[] subset = new SampleDimension[range.length];
+        for (int i=0; i<range.length; i++) {
+            final int j = range[i];
+            try {
+                subset[i] = dimensions.get(j);
+            } catch (IndexOutOfBoundsException e) {
+                throw new DataStoreException(invalidRange(j), e);
+            }
+        }
+        return UnmodifiableArrayList.wrap(subset);
+    }
+
+    /**
+     * Loads a subset of the grid coverage represented by this resource.
+     *
+     * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
+     * @param  range   0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
+     * @return the grid coverage for the specified domain and range.
+     * @throws DataStoreException if an error occurred while reading the grid coverage data.
+     */
+    @Override
+    public GridCoverage read(GridGeometry domain, int... range) throws DataStoreException {
+        if (domain == null) {
+            domain = query.getDomain();
+        }
+        range = query.subrange(this, range);
+        return source.read(domain, range);
+    }
+
+    /**
+     * Creates an exception message for an invalid range index.
+     */
+    final String invalidRange(final int index) {
+        return Resources.forLocale(getLocale()).getString(
+                Resources.Keys.InvalidSampleDimensionIndex_2, query.getRangeCount() - 1, index);
+    }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/SimpleQuery.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/SimpleQuery.java
index 8752f73..cb75e22 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/SimpleQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/SimpleQuery.java
@@ -48,11 +48,13 @@ import org.opengis.filter.sort.SortBy;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
+ *
+ * @todo Rename {@code FeatureQuery}.
  */
-public class SimpleQuery extends Query {
+public class SimpleQuery extends Query implements Cloneable {
     /**
      * Sentinel limit value for queries of unlimited length.
      * This value can be given to {@link #setLimit(long)} or retrieved from {@link #getLimit()}.
@@ -391,7 +393,7 @@ public class SimpleQuery extends Query {
      */
     public FeatureSet execute(final FeatureSet source) {
         ArgumentChecks.ensureNonNull("source", source);
-        return new FeatureSubset(source, this);
+        return new FeatureSubset(source, clone());
     }
 
     /**
@@ -416,6 +418,24 @@ public class SimpleQuery extends Query {
     }
 
     /**
+     * Returns a clone of this query.
+     *
+     * @return a clone of this query.
+     */
+    @Override
+    public SimpleQuery clone() {
+        /*
+         * Implementation note: no need to clone the arrays. It is safe to share the same array instances
+         * because this class does not modify them and does not return them directly to the user.
+         */
+        try {
+            return (SimpleQuery) super.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    /**
      * Returns a hash code value for this query.
      *
      * @return a hash value for this query.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/package-info.java
index abcd74e..a75a1c9 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/package-info.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/package-info.java
@@ -25,7 +25,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
index dc56851..a0dfe3b 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
@@ -20,6 +20,7 @@ import java.util.List;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.internal.storage.query.CoverageQuery;
 import org.apache.sis.util.ArgumentChecks;
 
 
@@ -37,6 +38,7 @@ import org.apache.sis.util.ArgumentChecks;
  * A coverage resource may be a member of {@link Aggregate} if a single file can provide many rasters.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ * @author  Johann Sorel (Geomatys)
  * @version 1.1
  * @since   1.0
  * @module
@@ -107,6 +109,12 @@ public interface GridCoverageResource extends DataSet {
 
     /**
      * Requests a subset of the coverage.
+     * The filtering can be applied in two aspects:
+     *
+     * <ul>
+     *   <li>The returned {@code GridCoverageResource} may contain a smaller domain (grid extent).</li>
+     *   <li>The returned {@code GridCoverageResource} may contain a smaller range (less sample dimensions).</li>
+     * </ul>
      *
      * <p>The returned subset may be a <em>view</em> of this set, i.e. changes in this {@code GridCoverageResource}
      * may be reflected immediately on the returned subset (and conversely), but not necessarily.
@@ -115,16 +123,21 @@ public interface GridCoverageResource extends DataSet {
      *
      * <p>The default implementation throws {@link UnsupportedQueryException}.</p>
      *
-     * @since   1.1
-     * @param  query  definition of space and sample dimension transforms applied at reading time.
+     * @param  query  definition of domain (grid extent) and range (sample dimensions) filtering applied at reading time.
      * @return resulting coverage resource (never {@code null}).
      * @throws UnsupportedQueryException if this {@code GridCoverageResource} can not execute the given query.
      *         This includes query validation errors.
      * @throws DataStoreException if another error occurred while processing the query.
+     *
+     * @since 1.1
      */
-    default GridCoverageResource subset(Query query) throws UnsupportedQueryException, DataStoreException {
+    default GridCoverageResource subset(final Query query) throws UnsupportedQueryException, DataStoreException {
         ArgumentChecks.ensureNonNull("query", query);
-        throw new UnsupportedQueryException();
+        if (query instanceof CoverageQuery) {
+            return ((CoverageQuery) query).execute(this);
+        } else {
+            throw new UnsupportedQueryException();
+        }
     }
 
     /**
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 2527de7..39cf050 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
@@ -21,7 +21,8 @@ import org.opengis.feature.Feature;
 
 /**
  * Definition of filtering to apply for fetching a resource subset.
- * Filtering can happen in two domains:
+ * 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>
@@ -49,6 +50,7 @@ import org.opengis.feature.Feature;
  * @version 1.0
  *
  * @see FeatureSet#subset(Query)
+ * @see GridCoverageResource#subset(Query)
  *
  * @since 0.8
  * @module
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java
index a20268b..f4ad848 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/package-info.java
@@ -26,7 +26,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */