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 2018/11/19 18:40:20 UTC

[sis] branch geoapi-4.0 updated: NetcdfStore.components() now include GridCoverageResources.

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 16c8dca  NetcdfStore.components() now include GridCoverageResources.
16c8dca is described below

commit 16c8dcae3cef957b391e6c43adf4732ed6665563
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Nov 19 16:38:36 2018 +0100

    NetcdfStore.components() now include GridCoverageResources.
---
 .../internal/referencing/ServicesForMetadata.java  |   2 +
 .../sis/storage/geotiff/ImageFileDirectory.java    |   5 +-
 .../org/apache/sis/internal/netcdf/Decoder.java    |   8 ++
 .../java/org/apache/sis/internal/netcdf/Grid.java  |   2 +
 .../org/apache/sis/internal/netcdf/Variable.java   |  31 ++++--
 .../sis/internal/netcdf/impl/FeaturesInfo.java     |  10 ++
 .../sis/internal/netcdf/impl/VariableInfo.java     |  24 ++++-
 .../sis/internal/netcdf/ucar/GridWrapper.java      |   2 +-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  30 +++++-
 .../apache/sis/storage/netcdf/GridResource.java    | 118 +++++++++++++++++++++
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |  11 +-
 .../apache/sis/internal/netcdf/VariableTest.java   |  12 +--
 .../org/apache/sis/internal/sql/feature/Table.java |   4 +-
 .../sis/internal/storage/AbstractFeatureSet.java   |  39 ++++++-
 .../sis/internal/storage/AbstractGridResource.java |  14 +++
 .../sis/internal/storage/AbstractResource.java     |  76 ++++++++-----
 .../sis/internal/storage/MemoryFeatureSet.java     |  21 ++--
 .../sis/internal/storage/MetadataBuilder.java      |   5 +
 .../apache/sis/storage/GridCoverageResource.java   |   2 +-
 .../sis/internal/storage/JoinFeatureSetTest.java   |   4 +-
 .../internal/storage/query/SimpleQueryTest.java    |   2 +-
 21 files changed, 360 insertions(+), 62 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
index b563dd4..1dba7a3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
@@ -371,6 +371,8 @@ public final class ServicesForMetadata extends ReferencingServices {
      * @param  envelope  the source envelope.
      * @param  target    the target extent where to store envelope information.
      * @throws TransformException if a coordinate transformation was required and failed.
+     * @throws UnsupportedOperationException if this method requires an Apache SIS module
+     *         which has been found on the classpath.
      */
     @Override
     public void addElements(final Envelope envelope, final DefaultExtent target) throws TransformException {
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 4cc3df0..a0be1a4 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -25,7 +25,6 @@ import java.util.logging.LogRecord;
 import java.nio.charset.Charset;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
-import org.opengis.metadata.Metadata;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.util.FactoryException;
 import org.opengis.util.GenericName;
@@ -1210,7 +1209,8 @@ final class ImageFileDirectory extends AbstractGridResource {
     }
 
     @Override
-    public Metadata getMetadata() throws DataStoreContentException {
+    protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException {
+        super.createMetadata(metadata);
         /*
          * TODO:
          *   - Modify ImageFileDirectory.completeMetadata(…) with the addition of a boolean telling that
@@ -1222,7 +1222,6 @@ final class ImageFileDirectory extends AbstractGridResource {
          *   - Invoke that method from here if GeoTiffStore already has a metadata, or conversely from
          *     GeoTiffStore if ImageResource already has a metadata.
          */
-        return null;
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index 0d72170..90756d7 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@ -21,6 +21,7 @@ import java.util.Objects;
 import java.util.Collection;
 import java.io.Closeable;
 import java.io.IOException;
+import java.nio.file.Path;
 import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
 import org.opengis.referencing.datum.Datum;
@@ -51,6 +52,13 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
     public static final String FORMAT_NAME = "netCDF";
 
     /**
+     * The path to the netCDF file, or {@code null} if unknown.
+     * This is set by netCDF store constructor and shall not be modified afterward.
+     * This is used for information purpose only, not for actual reading operation.
+     */
+    public Path location;
+
+    /**
      * The data store identifier created from the global attributes, or {@code null} if none.
      * Defined as a namespace for use as the scope of children resources (the variables).
      * This is set by netCDF store constructor and shall not be modified afterward.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
index 9583249..073dd59 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
@@ -111,6 +111,8 @@ public abstract class Grid extends NamedElement {
      * This method may return {@code null} if the grid shape can not be determined.
      *
      * @return number of cells along each source dimension, in "natural" (opposite of netCDF) order, or {@code null}.
+     *
+     * @see Variable#getShape()
      */
     protected abstract long[] getShape();
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
index 0c334d5..24c82c1 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
@@ -166,7 +166,7 @@ public abstract class Variable extends NamedElement {
     public final String getDataTypeName() {
         final StringBuilder buffer = new StringBuilder(20);
         buffer.append(getDataType().name().toLowerCase());
-        final int[] shape = getGridEnvelope();
+        final int[] shape = getShape();
         for (int i=shape.length; --i>=0;) {
             buffer.append('[').append(Integer.toUnsignedLong(shape[i])).append(']');
         }
@@ -203,7 +203,7 @@ public abstract class Variable extends NamedElement {
      */
     public final boolean isCoverage(final int minSpan) {
         int numVectors = 0;                                     // Number of dimension having more than 1 value.
-        for (final int length : getGridEnvelope()) {
+        for (final int length : getShape()) {
             if (Integer.toUnsignedLong(length) >= minSpan) {
                 numVectors++;
             }
@@ -226,8 +226,21 @@ public abstract class Variable extends NamedElement {
     public abstract boolean isCoordinateSystemAxis();
 
     /**
+     * Returns the grid geometry for this variable, or {@code null} if this variable is not a data cube.
+     * Not all variables have a grid geometry. For example collections of features do not have such grid.
+     * The same grid geometry may be shared by many variables.
+     *
+     * @param  decoder  the decoder to use for constructing the grid geometry if needed.
+     * @return the grid geometry for this variable, or {@code null} if none.
+     * @throws IOException if an error occurred while reading the data.
+     * @throws DataStoreException if a logical error occurred.
+     */
+    public abstract Grid getGridGeometry(Decoder decoder) throws IOException, DataStoreException;
+
+    /**
      * Returns the names of the dimensions of this variable, in the order they are declared in the netCDF file.
      * The dimensions are those of the grid, not the dimensions of the coordinate system.
+     * This information is used for completing ISO 19115 metadata.
      *
      * @return the names of all dimension of the grid, in netCDF order (reverse of "natural" order).
      */
@@ -236,13 +249,17 @@ public abstract class Variable extends NamedElement {
     /**
      * Returns the length (number of cells) of each grid dimension, in the order they are declared in the netCDF file.
      * The length of this array shall be equals to the length of the {@link #getGridDimensionNames()} array.
+     * Values shall be handled as unsigned 32 bits integers.
      *
      * <p>In ISO 19123 terminology, this method returns the upper corner of the grid envelope plus one.
-     * The lower corner is always (0, 0, …, 0).</p>
+     * The lower corner is always (0, 0, …, 0). This method is used by {@link #isCoverage(int)} method,
+     * or for building string representations of this variable.</p>
+     *
+     * @return the number of grid cells for each dimension, as unsigned integer in netCDF order (reverse of "natural" order).
      *
-     * @return the number of grid cells for each dimension, in netCDF order (reverse of "natural" order).
+     * @see Grid#getShape()
      */
-    public abstract int[] getGridEnvelope();
+    public abstract int[] getShape();
 
     /**
      * Returns the names of all attributes associated to this variable.
@@ -310,7 +327,7 @@ public abstract class Variable extends NamedElement {
      * Constraints on the argument values are:
      *
      * <ul>
-     *   <li>All arrays length shall be equal to the length of the {@link #getGridEnvelope()} array.</li>
+     *   <li>All arrays length shall be equal to the length of the {@link #getShape()} array.</li>
      *   <li>For each index <var>i</var>, value of {@code area[i]} shall be in the range from 0 inclusive
      *       to {@code Integer.toUnsignedLong(getGridEnvelope()[i])} exclusive.</li>
      * </ul>
@@ -418,7 +435,7 @@ public abstract class Variable extends NamedElement {
     @Override
     public String toString() {
         final StringBuilder buffer = new StringBuilder(getName()).append(" : ").append(getDataType());
-        final int[] shape = getGridEnvelope();
+        final int[] shape = getShape();
         for (int i=shape.length; --i>=0;) {
             buffer.append('[').append(Integer.toUnsignedLong(shape[i])).append(']');
         }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
index dbc84e1..149443c 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
@@ -293,6 +293,16 @@ search: for (final VariableInfo counts : decoder.variables) {
     }
 
     /**
+     * Returns the number of features in this set.
+     *
+     * @return the number of features.
+     */
+    @Override
+    protected Integer getFeatureCount() {
+        return counts.size();
+    }
+
+    /**
      * Returns the stream of features.
      *
      * @param  parallel  ignored, since current version does not support parallelism.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
index 8712326..524716c 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
@@ -28,12 +28,14 @@ import ucar.nc2.constants.CDM;
 import ucar.nc2.constants._Coordinate;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.DataType;
+import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.netcdf.Resources;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.storage.io.HyperRectangleReader;
 import org.apache.sis.internal.storage.io.Region;
 import org.apache.sis.internal.util.StandardDateFormat;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.util.logging.WarningListeners;
@@ -137,6 +139,8 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
      * The grid geometry associated to this variable,
      * computed by {@link ChannelDecoder#getGridGeometries()} when first needed.
      * May stay {@code null} if the variable is not a data cube.
+     *
+     * @see #getGridGeometry(Decoder)
      */
     GridInfo gridGeometry;
 
@@ -416,8 +420,25 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     }
 
     /**
+     * Returns the grid geometry for this variable, or {@code null} if this variable is not a data cube.
+     * The grid geometries are opportunistically cached in {@code VariableInfo} instances after they have
+     * been computed by {@link ChannelDecoder#getGridGeometries()}.
+     * The same grid geometry may be shared by many variables.
+     *
+     * @see ChannelDecoder#getGridGeometries()
+     */
+    @Override
+    public Grid getGridGeometry(final Decoder decoder) throws IOException, DataStoreException {
+        if (gridGeometry == null) {
+            decoder.getGridGeometries();            // Force calculation of grid geometries if not already done.
+        }
+        return gridGeometry;
+    }
+
+    /**
      * Returns the names of the dimensions of this variable.
      * The dimensions are those of the grid, not the dimensions of the coordinate system.
+     * This information is used for completing ISO 19115 metadata.
      */
     @Override
     public String[] getGridDimensionNames() {
@@ -431,11 +452,12 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     /**
      * Returns the length (number of cells) of each grid dimension. In ISO 19123 terminology, this method
      * returns the upper corner of the grid envelope plus one. The lower corner is always (0,0,…,0).
+     * This method is used mostly for building string representations of this variable.
      *
      * @return the number of grid cells for each dimension, as unsigned integers.
      */
     @Override
-    public int[] getGridEnvelope() {
+    public int[] getShape() {
         final int[] shape = new int[dimensions.length];
         for (int i=0; i<shape.length; i++) {
             shape[i] = dimensions[i].length;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
index 8e34bf5..2787b3b 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
@@ -52,7 +52,7 @@ final class GridWrapper extends Grid {
     /**
      * The netCDF coordinate system to wrap.
      */
-    private final CoordinateSystem netcdfCS;
+    final CoordinateSystem netcdfCS;
 
     /**
      * Creates a new grid geometry for the given netCDF coordinate system.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
index 560594e..10bde94 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
@@ -27,6 +27,7 @@ import ucar.ma2.InvalidRangeException;
 import ucar.nc2.Attribute;
 import ucar.nc2.Dimension;
 import ucar.nc2.VariableIF;
+import ucar.nc2.dataset.Enhancements;
 import ucar.nc2.dataset.VariableEnhanced;
 import ucar.nc2.dataset.CoordinateAxis1D;
 import ucar.nc2.units.SimpleUnit;
@@ -34,11 +35,14 @@ import ucar.nc2.units.DateUnit;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.math.Vector;
 import org.apache.sis.internal.netcdf.DataType;
+import org.apache.sis.internal.netcdf.Decoder;
+import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.measure.Units;
+import ucar.nc2.dataset.CoordinateSystem;
 
 
 /**
@@ -198,8 +202,31 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * Returns the grid geometry for this variable, or {@code null} if this variable is not a data cube.
+     * This method searches for a grid previously computed by {@link DecoderWrapper#getGridGeometries()}.
+     * The same grid geometry may be shared by many variables.
+     *
+     * @see DecoderWrapper#getGridGeometries()
+     */
+    @Override
+    public Grid getGridGeometry(final Decoder decoder) throws IOException, DataStoreException {
+        if (variable instanceof Enhancements) {
+            final List<CoordinateSystem> cs = ((Enhancements) variable).getCoordinateSystems();
+            if (cs != null && !cs.isEmpty()) {
+                for (final Grid grid : decoder.getGridGeometries()) {
+                    if (cs.contains(((GridWrapper) grid).netcdfCS)) {
+                        return grid;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns the names of the dimensions of this variable.
      * The dimensions are those of the grid, not the dimensions of the coordinate system.
+     * This information is used for completing ISO 19115 metadata.
      */
     @Override
     public String[] getGridDimensionNames() {
@@ -214,9 +241,10 @@ final class VariableWrapper extends Variable {
     /**
      * Returns the length (number of cells) of each grid dimension. In ISO 19123 terminology, this method
      * returns the upper corner of the grid envelope plus one. The lower corner is always (0,0,…,0).
+     * This method is used mostly for building string representations of this variable.
      */
     @Override
-    public int[] getGridEnvelope() {
+    public int[] getShape() {
         return variable.getShape();
     }
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
new file mode 100644
index 0000000..30b2a28
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
@@ -0,0 +1,118 @@
+/*
+ * 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.storage.netcdf;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.opengis.util.GenericName;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.internal.netcdf.Decoder;
+import org.apache.sis.internal.netcdf.Grid;
+import org.apache.sis.internal.netcdf.Variable;
+import org.apache.sis.internal.storage.AbstractGridResource;
+import org.apache.sis.internal.storage.ResourceOnFileSystem;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.Resource;
+
+
+/**
+ * A grid coverage in a netCDF file.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class GridResource extends AbstractGridResource implements ResourceOnFileSystem {
+    /**
+     * The identifier of this grid resource.
+     * This is the variable name.
+     */
+    private final GenericName identifier;
+
+    /**
+     * The grid geometry (size, CRS…) of the {@linkplain #data} cube.
+     */
+    private final GridGeometry gridGeometry;
+
+    /**
+     * The netCDF variable wrapped by this resource.
+     */
+    private final Variable data;
+
+    /**
+     * Path to the netCDF file for information purpose, or {@code null} if unknown.
+     */
+    private final Path location;
+
+    /**
+     * Creates a new resource.
+     *
+     * @param  decoder  the implementation used for decoding the netCDF file.
+     * @param  grid     the grid geometry (size, CRS…) of the {@linkplain #data} cube.
+     * @param  data     the variable providing actual data.
+     */
+    private GridResource(final Decoder decoder, final Grid grid, final Variable data) throws IOException, DataStoreException {
+        super(decoder.listeners);
+        this.data    = data;
+        gridGeometry = grid.getGridGeometry(decoder);
+        identifier   = decoder.nameFactory.createLocalName(decoder.namespace, data.getName());
+        location     = decoder.location;
+    }
+
+    /**
+     * Returns a list of all grid resources found in the netCDF file opened by the given decoder.
+     * This method should be invoked only once and the result cached. The returned list is modifiable;
+     * caller is free to add other elements.
+     */
+    static List<Resource> list(final Decoder decoder) throws IOException, DataStoreException {
+        final List<Resource> resources = new ArrayList<>();
+        for (final Variable variable : decoder.getVariables()) {
+            final Grid grid = variable.getGridGeometry(decoder);
+            if (grid != null) {
+                resources.add(new GridResource(decoder, grid, variable));
+            }
+        }
+        return resources;
+    }
+
+    /**
+     * Returns the variable name as an identifier of this resource.
+     */
+    @Override
+    public GenericName getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * Returns an object containing the grid size, the CRS and the conversion from grid indices to CRS coordinates.
+     */
+    @Override
+    public GridGeometry getGridGeometry() throws DataStoreException {
+        return gridGeometry;
+    }
+
+    /**
+     * Gets the paths to files used by this resource, or an empty array if unknown.
+     */
+    @Override
+    public Path[] getComponentFiles() throws DataStoreException {
+        return (location != null) ? new Path[] {location} : new Path[0];
+    }
+}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
index 8176dca..5b4c071 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
@@ -17,6 +17,7 @@
 package org.apache.sis.storage.netcdf;
 
 import java.io.IOException;
+import java.nio.file.Path;
 import java.net.URI;
 import java.util.List;
 import java.util.Collection;
@@ -97,6 +98,7 @@ public class NetcdfStore extends DataStore implements Aggregate {
     public NetcdfStore(final NetcdfStoreProvider provider, final StorageConnector connector) throws DataStoreException {
         super(provider, connector);
         location = connector.getStorageAs(URI.class);
+        final Path path = connector.getStorageAs(Path.class);
         try {
             decoder = NetcdfStoreProvider.decoder(listeners, connector);
         } catch (IOException | ArithmeticException e) {
@@ -106,6 +108,7 @@ public class NetcdfStore extends DataStore implements Aggregate {
             throw new UnsupportedStorageException(super.getLocale(), NetcdfStoreProvider.NAME,
                     connector.getStorage(), connector.getOption(OptionKey.OPEN_OPTIONS));
         }
+        decoder.location = path;
         String id = decoder.stringValue(ACDD.id);
         if (id == null || (id = id.trim()).isEmpty()) {
             id = decoder.getFilename();
@@ -199,7 +202,13 @@ public class NetcdfStore extends DataStore implements Aggregate {
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public synchronized Collection<Resource> components() throws DataStoreException {
         if (components == null) try {
-            components = UnmodifiableArrayList.wrap(decoder.getDiscreteSampling());
+            Resource[] resources = decoder.getDiscreteSampling();
+            final List<Resource> list = GridResource.list(decoder);
+            if (!list.isEmpty()) {
+                list.addAll(UnmodifiableArrayList.wrap(resources));
+                resources = list.toArray(new Resource[list.size()]);
+            }
+            components = UnmodifiableArrayList.wrap(resources);
         } catch (IOException e) {
             throw new DataStoreException(e);
         }
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java
index 7782f4c..8ebdb20 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java
@@ -89,7 +89,7 @@ public strictfp class VariableTest extends TestCase {
      *   <li>{@link Variable#getName()}</li>
      *   <li>{@link Variable#getDescription()}</li>
      *   <li>{@link Variable#getDataType()}</li>
-     *   <li>{@link Variable#getGridEnvelope()} length</li>
+     *   <li>{@link Variable#getShape()} length</li>
      *   <li>{@link Variable#isCoordinateSystemAxis()}</li>
      *   <li>{@link Variable#isCoverage(int)}</li>
      * </ul>
@@ -126,7 +126,7 @@ public strictfp class VariableTest extends TestCase {
             assertEquals(name, expected[propertyIndex++], name);
             assertEquals(name, expected[propertyIndex++], variable.getDescription());
             assertEquals(name, expected[propertyIndex++], dataType);
-            assertEquals(name, expected[propertyIndex++], variable.getGridEnvelope().length);
+            assertEquals(name, expected[propertyIndex++], variable.getShape().length);
             assertEquals(name, expected[propertyIndex++], variable.isCoordinateSystemAxis());
             assertEquals(name, expected[propertyIndex++], variable.isCoverage(2));
             assertEquals(0, propertyIndex % NUM_BASIC_PROPERTY_COLUMNS);            // Sanity check for VariableTest itself.
@@ -137,7 +137,7 @@ public strictfp class VariableTest extends TestCase {
     }
 
     /**
-     * Tests {@link Variable#getGridDimensionNames()} and {@link Variable#getGridEnvelope()}
+     * Tests {@link Variable#getGridDimensionNames()} and {@link Variable#getShape()}
      * on a simple two-dimensional dataset.
      *
      * @throws IOException if an I/O error occurred while opening the file.
@@ -154,11 +154,11 @@ public strictfp class VariableTest extends TestCase {
 
         assertArrayEquals("getGridEnvelope()", new int[] {
             73, 73
-        }, variable.getGridEnvelope());
+        }, variable.getShape());
     }
 
     /**
-     * Tests {@link Variable#getGridDimensionNames()} and {@link Variable#getGridEnvelope()}
+     * Tests {@link Variable#getGridDimensionNames()} and {@link Variable#getShape()}
      * on a compound four-dimensional dataset.
      *
      * @throws IOException if an I/O error occurred while opening the file.
@@ -175,7 +175,7 @@ public strictfp class VariableTest extends TestCase {
 
         assertArrayEquals("getGridEnvelope()", new int[] {
             1, 4, 19, 38
-        }, variable.getGridEnvelope());
+        }, variable.getShape());
     }
 
     /**
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 e41af4e..1589506 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
@@ -570,8 +570,8 @@ final class Table extends AbstractFeatureSet {
      * change at any time.
      *
      * @param  metadata     information about the database.
-     * @param  approximate  whether approximative or outdated values are acceptable.
-     * @return number of rows (may be approximative), or -1 if unknown.
+     * @param  approximate  whether approximate or outdated values are acceptable.
+     * @return number of rows (may be approximate), or -1 if unknown.
      */
     final long countRows(final DatabaseMetaData metadata, final boolean approximate) throws SQLException {
         long count = -1;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
index e386c4e..fe7ad8c 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
@@ -30,7 +30,20 @@ import org.opengis.feature.FeatureType;
 
 
 /**
- * Base implementation of feature sets contained in data stores.
+ * Base implementation of feature sets contained in data stores. This class provides a {@link #getMetadata()}
+ * which extracts information from other methods. Subclasses shall or should override the following methods:
+ *
+ * <ul>
+ *   <li>{@link #getType()} (mandatory)</li>
+ *   <li>{@link #getFeatureCount()} (recommended)</li>
+ *   <li>{@link #getEnvelope()} (recommended)</li>
+ *   <li>{@link #createMetadata(MetadataBuilder)} (optional)</li>
+ *   <li>{@link #features(boolean parallel)} (mandatory)</li>
+ * </ul>
+ *
+ * {@section Thread safety}
+ * Default methods of this abstract class are thread-safe.
+ * Synchronization, when needed, uses {@code this} lock.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -76,6 +89,30 @@ public abstract class AbstractFeatureSet extends AbstractResource implements Fea
     }
 
     /**
+     * Returns an estimation of the number of features in this set, or {@code null} if unknown.
+     * The default implementation returns {@code null}.
+     *
+     * @return estimation of the number of features, or {@code null}.
+     */
+    protected Integer getFeatureCount() {
+        return null;
+    }
+
+    /**
+     * Invoked the first time that {@link #getMetadata()} is invoked. The default implementation populates metadata
+     * based on information provided by {@link #getType()}, {@link #getIdentifier()} and {@link #getEnvelope()}.
+     * Subclasses should override if they can provide more information.
+     *
+     * @param  metadata  the builder where to set metadata properties.
+     * @throws DataStoreException if an error occurred while reading metadata from the data store.
+     */
+    @Override
+    protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException {
+        super.createMetadata(metadata);
+        metadata.addFeatureType(getType(), getFeatureCount());
+    }
+
+    /**
      * Requests a subset of features and/or feature properties from this resource.
      * The default implementation try to execute the queries by filtering the
      * {@linkplain #features(boolean) stream of features}, which may be inefficient.
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 9d967bb..5590872 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
@@ -68,4 +68,18 @@ public abstract class AbstractGridResource extends AbstractResource implements G
         }
         return null;
     }
+
+    /**
+     * Invoked the first time that {@link #getMetadata()} is invoked. The default implementation populates
+     * metadata based on information provided by {@link #getIdentifier()} and {@link #getGridGeometry()}.
+     * Subclasses should override if they can provide more information.
+     *
+     * @param  metadata  the builder where to set metadata properties.
+     * @throws DataStoreException if an error occurred while reading metadata from the data store.
+     */
+    @Override
+    protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException {
+        super.createMetadata(metadata);
+        metadata.addSpatialRepresentation(null, getGridGeometry(), false);
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
index 69f973f..d482384 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
@@ -20,6 +20,7 @@ import java.util.Locale;
 import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.opengis.geometry.Envelope;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.util.Localized;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.storage.Resource;
@@ -27,16 +28,21 @@ import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
-import org.apache.sis.metadata.iso.DefaultMetadata;
-import org.apache.sis.metadata.iso.citation.DefaultCitation;
-import org.apache.sis.metadata.iso.identification.DefaultDataIdentification;
 
 
 /**
- * Base implementation of resources contained in data stores.
- * This class provides default implementation of {@link #getIdentifier()} and {@link #getEnvelope()}
- * methods which extract their information from the value returned by {@link #getMetadata()}.
- * Subclasses should override those methods if they can provide those information more efficiently.
+ * Base implementation of resources contained in data stores. This class provides a {@link #getMetadata()}
+ * which extracts information from other methods. Subclasses shall or should override the following methods:
+ *
+ * <ul>
+ *   <li>{@link #getIdentifier()} (mandatory)</li>
+ *   <li>{@link #getEnvelope()} (recommended)</li>
+ *   <li>{@link #createMetadata(MetadataBuilder)} (optional)</li>
+ * </ul>
+ *
+ * {@section Thread safety}
+ * Default methods of this abstract class are thread-safe.
+ * Synchronization, when needed, uses {@code this} lock.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
@@ -47,17 +53,14 @@ public abstract class AbstractResource implements Resource, Localized {
     /**
      * A description of this resource as an unmodifiable metadata, or {@code null} if not yet computed.
      * If non-null, this metadata shall contain at least the resource {@linkplain #getIdentifier() identifier}.
-     *
-     * <p>Those metadata are created by {@link #getMetadata()} when first needed.
-     * Subclasses can set a value to this field directly if they wish to bypass the
-     * default metadata creation process.</p>
+     * Those metadata are created by {@link #getMetadata()} when first needed.
      */
-    protected Metadata metadata;
+    private Metadata metadata;
 
     /**
      * The set of registered warning listeners for the data store, or {@code null} if none.
      */
-    protected final WarningListeners<DataStore> listeners;
+    private final WarningListeners<DataStore> listeners;
 
     /**
      * Creates a new resource.
@@ -102,11 +105,11 @@ public abstract class AbstractResource implements Resource, Localized {
     }
 
     /**
-     * Returns the spatio-temporal envelope of this resource.
-     * The default implementation returns {@code null}.
+     * Returns the spatio-temporal envelope of this resource. This information is part of API only in some kind of resources
+     * like {@link org.apache.sis.storage.FeatureSet}. But the method is provided in this base class for convenience and for
+     * allowing {@link #getMetadata()} to use this information if available. The default implementation returns {@code null}.
      *
      * @return the spatio-temporal resource extent, or {@code null} if none.
-     *
      * @throws DataStoreException if an error occurred while reading or computing the envelope.
      */
     public Envelope getEnvelope() throws DataStoreException {
@@ -114,26 +117,43 @@ public abstract class AbstractResource implements Resource, Localized {
     }
 
     /**
-     * Returns a description of this set of features.
-     * Current implementation sets only the resource name; this may change in any future Apache SIS version.
+     * Returns a description of this resource. This method invokes {@link #createMetadata(MetadataBuilder)}
+     * the first time it is invoked, then cache the result.
+     *
+     * @return information about this resource (never {@code null} in this implementation).
+     * @throws DataStoreException if an error occurred while reading or computing the envelope.
      */
     @Override
-    public synchronized Metadata getMetadata() throws DataStoreException {
+    public final synchronized Metadata getMetadata() throws DataStoreException {
         if (metadata == null) {
-            final DefaultMetadata metadata = new DefaultMetadata();
-            final GenericName name = getIdentifier();
-            if (name != null) {                         // Paranoiac check (should never be null).
-                final DefaultCitation citation = new DefaultCitation(name.toInternationalString());
-                final DefaultDataIdentification identification = new DefaultDataIdentification();
-                identification.setCitation(citation);
-            }
-            metadata.transition(DefaultMetadata.State.FINAL);
-            this.metadata = metadata;
+            final MetadataBuilder builder = new MetadataBuilder();
+            createMetadata(builder);
+            metadata = builder.build(true);
         }
         return metadata;
     }
 
     /**
+     * Invoked the first time that {@link #getMetadata()} is invoked. The default implementation populates
+     * metadata based on information provided by {@link #getIdentifier()} and {@link #getEnvelope()}.
+     * Subclasses should override if they can provide more information.
+     *
+     * @param  metadata  the builder where to set metadata properties.
+     * @throws DataStoreException if an error occurred while reading metadata from the data store.
+     */
+    protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException {
+        final GenericName name = getIdentifier();
+        if (name != null) {
+            metadata.addTitle(name.toInternationalString());
+        }
+        try {
+            metadata.addExtent(getEnvelope());
+        } catch (TransformException | UnsupportedOperationException e) {
+            listeners.warning(null, e);
+        }
+    }
+
+    /**
      * Ignored in current implementation, on the assumption that most resources produce no events.
      *
      * @param  <T>        {@inheritDoc}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
index 32a911d..fb21125 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
@@ -18,7 +18,6 @@ package org.apache.sis.internal.storage;
 
 import java.util.Collection;
 import java.util.stream.Stream;
-import org.opengis.metadata.Metadata;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.WarningListeners;
@@ -29,8 +28,8 @@ import org.opengis.feature.FeatureType;
 
 
 /**
- * Set of features stored in memory.
- * Metadata and features are specified at construction time.
+ * Set of features stored in memory. Features are specified at construction time.
+ * Metadata can be specified by overriding {@link #createMetadata(MetadataBuilder)}.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -38,7 +37,7 @@ import org.opengis.feature.FeatureType;
  * @since   1.0
  * @module
  */
-public final class MemoryFeatureSet extends AbstractFeatureSet {
+public class MemoryFeatureSet extends AbstractFeatureSet {
     /**
      * The type specified at construction time and returned by {@link #getType()}.
      */
@@ -56,11 +55,10 @@ public final class MemoryFeatureSet extends AbstractFeatureSet {
      * (this is not verified).
      *
      * @param listeners  the set of registered warning listeners for the data store, or {@code null} if none.
-     * @param metadata   information about this resource, or {@code null} for inferring default metadata.
      * @param type       the type of all features in the given collection.
      * @param features   collection of stored features. This collection will not be copied.
      */
-    public MemoryFeatureSet(final WarningListeners<DataStore> listeners, Metadata metadata,
+    public MemoryFeatureSet(final WarningListeners<DataStore> listeners,
                             final FeatureType type, final Collection<Feature> features)
     {
         super(listeners);
@@ -68,7 +66,6 @@ public final class MemoryFeatureSet extends AbstractFeatureSet {
         ArgumentChecks.ensureNonNull("features", features);
         this.type     = type;
         this.features = features;
-        this.metadata = metadata;
     }
 
     /**
@@ -82,6 +79,16 @@ public final class MemoryFeatureSet extends AbstractFeatureSet {
     }
 
     /**
+     * Returns the number of features in this set.
+     *
+     * @return the number of features.
+     */
+    @Override
+    protected Integer getFeatureCount() {
+        return features.size();
+    }
+
+    /**
      * Returns a stream of all features contained in this dataset.
      *
      * @param  parallel  {@code true} for a parallel stream (if supported), or {@code false} for a sequential stream.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
index 336302c..2ff8150 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
@@ -1910,7 +1910,12 @@ parse:      for (int i = 0; i < length;) {
      *   <li>{@code metadata/referenceSystemInfo}</li>
      * </ul>
      *
+     * This method does not add the envelope provided by {@link GridGeometry#getEnvelope()}.
+     * That envelope appears in a separated node, which can be added by {@link #addExtent(Envelope)}.
+     * This separation is required by {@link AbstractGridResource} for instance.
+     *
      * @param  description    a general description of the "grid to CRS" transformation, or {@code null} if none.
+     *                        Can also be specified later by a call to {@link #setGridToCRS(CharSequence)}.
      * @param  grid           the grid extent, "grid to CRS" transform and target CRS, or {@code null} if none.
      * @param  addResolution  whether to declare the resolutions. Callers should set this argument to {@code false} if they intend
      *                        to provide the resolution themselves, or if grid axes are not in the same order than CRS axes.
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 903c341..4945014 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
@@ -28,7 +28,7 @@ import org.apache.sis.coverage.grid.GridGeometry;
  * @since   1.0
  * @module
  */
-public interface GridCoverageResource extends Resource {
+public interface GridCoverageResource extends DataSet {
     /**
      * Returns the valid extent of grid coordinates together with the transform
      * from those grid coordinates to real world coordinates.
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/JoinFeatureSetTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/JoinFeatureSetTest.java
index 8234b50..fada5a1 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/JoinFeatureSetTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/JoinFeatureSetTest.java
@@ -70,7 +70,7 @@ public final strictfp class JoinFeatureSetTest extends TestCase {
         builder.addAttribute( String.class).setName("myNameSpace", "att1");
         builder.addAttribute(Integer.class).setName("myNameSpace", "att2");
         final FeatureType type1 = builder.build();
-        featureSet1 = new MemoryFeatureSet(null, null, type1, Arrays.asList(
+        featureSet1 = new MemoryFeatureSet(null, type1, Arrays.asList(
                 newFeature1(type1, "fid_1_0", "str1",   1),
                 newFeature1(type1, "fid_1_1", "str2",   2),
                 newFeature1(type1, "fid_1_2", "str3",   3),
@@ -82,7 +82,7 @@ public final strictfp class JoinFeatureSetTest extends TestCase {
         builder.addAttribute(Integer.class).setName("otherNameSpace", "att3");
         builder.addAttribute( Double.class).setName("otherNameSpace", "att4");
         final FeatureType type2 = builder.build();
-        featureSet2 = new MemoryFeatureSet(null, null, type2, Arrays.asList(
+        featureSet2 = new MemoryFeatureSet(null, type2, Arrays.asList(
                 newFeature2(type2, "fid_2_0",  1, 10),
                 newFeature2(type2, "fid_2_1",  2, 20),
                 newFeature2(type2, "fid_2_2",  2, 30),
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/query/SimpleQueryTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/query/SimpleQueryTest.java
index 4f7ba90..f29bb82 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/query/SimpleQueryTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/query/SimpleQueryTest.java
@@ -80,7 +80,7 @@ public final strictfp class SimpleQueryTest extends TestCase {
             feature(type, 1, 1),
             feature(type, 4, 1)
         };
-        featureSet = new MemoryFeatureSet(null, null, type, Arrays.asList(features));
+        featureSet = new MemoryFeatureSet(null, type, Arrays.asList(features));
         query      = new SimpleQuery();
     }