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/12/10 01:29:06 UTC

[sis] branch geoapi-4.0 updated (000b6e0 -> 5a18b91)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 000b6e0  Implements GridCoverageResource.getSampleDimension() in GeoTIFF reader too. It provides us more tests.
     new 5328290  Reimplement GeoTiffStore.components() as a List instead than a Set.
     new 5a18b91  First draft of a GridResource.read(…) method returning a GridCoverage.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sis/util/iso/DefaultTypeName.java   |   4 +-
 .../main/java/org/apache/sis/util/iso/Names.java   |  44 ++++
 .../org/apache/sis/coverage/SampleDimension.java   |  57 +++--
 .../org/apache/sis/coverage/SampleRangeFormat.java |   8 +-
 .../main/java/org/apache/sis/coverage/ToNaN.java   |   8 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java | 128 ++++++++++
 .../org/apache/sis/coverage/grid/GridGeometry.java |  36 ++-
 .../sis/internal/raster/ColorModelFactory.java     |  58 +++++
 .../apache/sis/internal/raster/RasterFactory.java  |  83 +++++++
 .../java/org/apache/sis/internal/jdk9/JDK9.java    |  25 +-
 .../sis/internal/util/ListOfUnknownSize.java       | 263 +++++++++++++++++++++
 .../apache/sis/internal/util/SetOfUnknownSize.java |  24 +-
 .../main/java/org/apache/sis/math/ArrayVector.java |  38 +++
 .../src/main/java/org/apache/sis/math/Vector.java  |  28 +++
 ...ilitiesTest.java => ListOfUnknownSizeTest.java} |  43 ++--
 .../apache/sis/test/suite/UtilityTestSuite.java    |   1 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  93 ++++----
 .../sis/storage/geotiff/ImageFileDirectory.java    |  14 ++
 .../org/apache/sis/internal/netcdf/DataType.java   |  33 +++
 .../apache/sis/storage/netcdf/GridResource.java    |  54 +++++
 .../java/org/apache/sis/storage/netcdf/Image.java  |  75 ++++++
 .../sis/internal/storage/AbstractGridResource.java |   6 +-
 .../sis/internal/storage/MetadataBuilder.java      |  70 ++++--
 .../apache/sis/storage/GridCoverageResource.java   |  18 ++
 24 files changed, 1084 insertions(+), 127 deletions(-)
 create mode 100644 core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
 create mode 100644 core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
 create mode 100644 core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
 create mode 100644 core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
 copy core/sis-utility/src/test/java/org/apache/sis/internal/util/{UtilitiesTest.java => ListOfUnknownSizeTest.java} (53%)
 create mode 100644 storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Image.java


[sis] 02/02: First draft of a GridResource.read(…) method returning a GridCoverage.

Posted by de...@apache.org.
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 5a18b91eed194a80ffe3fe7d9d11f774dd7d4ab1
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Dec 10 02:28:32 2018 +0100

    First draft of a GridResource.read(…) method returning a GridCoverage.
---
 .../org/apache/sis/util/iso/DefaultTypeName.java   |   4 +-
 .../main/java/org/apache/sis/util/iso/Names.java   |  44 +++++++
 .../org/apache/sis/coverage/SampleDimension.java   |  57 ++++++---
 .../org/apache/sis/coverage/SampleRangeFormat.java |   8 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java | 128 +++++++++++++++++++++
 .../org/apache/sis/coverage/grid/GridGeometry.java |  36 ++++--
 .../sis/internal/raster/ColorModelFactory.java     |  58 ++++++++++
 .../apache/sis/internal/raster/RasterFactory.java  |  83 +++++++++++++
 .../java/org/apache/sis/internal/jdk9/JDK9.java    |  25 +++-
 .../main/java/org/apache/sis/math/ArrayVector.java |  38 ++++++
 .../src/main/java/org/apache/sis/math/Vector.java  |  28 +++++
 .../sis/storage/geotiff/ImageFileDirectory.java    |  14 +++
 .../org/apache/sis/internal/netcdf/DataType.java   |  33 ++++++
 .../apache/sis/storage/netcdf/GridResource.java    |  54 +++++++++
 .../java/org/apache/sis/storage/netcdf/Image.java  |  75 ++++++++++++
 .../sis/internal/storage/AbstractGridResource.java |   6 +-
 .../sis/internal/storage/MetadataBuilder.java      |  70 +++++++----
 .../apache/sis/storage/GridCoverageResource.java   |  18 +++
 18 files changed, 720 insertions(+), 59 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java
index 7a0b96a..e730e38 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java
@@ -144,9 +144,9 @@ public class DefaultTypeName extends DefaultLocalName implements TypeName {
      * The value class to be returned by {@link #toClass()}, or {@code null} if not yet computed.
      * {@link Void#TYPE} is used as a sentinel value meaning explicit {@code null}.
      *
-     * <p>This value is only computed. We do not allow the user to explicitely specify it, because we
+     * <p>This value is only computed. We do not allow the user to explicitly specify it, because we
      * need that {@code DefaultTypeName}s having identical name also have the same {@code valueClass}.
-     * This is necessary {@link DefaultNameFactory#pool} cache integrity. Users who want to explicitely
+     * This is necessary {@link DefaultNameFactory#pool} cache integrity. Users who want to explicitly
      * specify their own value class can override {@link #toClass()} instead.</p>
      *
      * @see #setValueClass(NameSpace, String, Class)
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
index 7d3f083..3d130c9 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
@@ -86,6 +86,13 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  */
 public final class Names extends Static {
     /**
+     * Sequence numbers, created when first needed.
+     *
+     * @see #createMemberName(CharSequence, String, int)
+     */
+    private static final MemberName[] SEQUENCE_NUMBERS = new MemberName[16];
+
+    /**
      * Do not allow instantiation of this class.
      */
     private Names() {
@@ -269,6 +276,43 @@ public final class Names extends Static {
     }
 
     /**
+     * Creates a member name for the given sequence number. The member type will be {@code "OGC:Integer"}.
+     * This method can be used for {@linkplain org.apache.sis.metadata.iso.content.DefaultRangeDimension#setSequenceIdentifier
+     * setting band identifier in metadata} in the common case where band identifier are just numbers.
+     *
+     * @param  namespace  the namespace, or {@code null} for the global namespace.
+     * @param  separator  the separator between the namespace and the local part, or {@code null}
+     *                    for the {@linkplain DefaultNameSpace#DEFAULT_SEPARATOR default separator}.
+     * @param  localPart  the sequence number to use as local part.
+     * @return a member name in the given namespace with the given sequence number.
+     *
+     * @see org.opengis.metadata.content.RangeDimension#getSequenceIdentifier()
+     *
+     * @since 1.0
+     */
+    public static MemberName createMemberName(final CharSequence namespace, String separator, final int localPart) {
+        if (DefaultNameSpace.DEFAULT_SEPARATOR_STRING.equals(separator)) {
+            separator = null;       // For making test for caching easier.
+        }
+        final boolean cached = (namespace == null) && (separator == null) && localPart >= 0 && localPart < SEQUENCE_NUMBERS.length;
+        MemberName name = null;
+        if (cached) synchronized (SEQUENCE_NUMBERS) {
+            name = SEQUENCE_NUMBERS[localPart];
+        }
+        if (name == null) {
+            name = createMemberName(namespace, separator, Integer.toString(localPart), Integer.class);
+            if (cached) synchronized (SEQUENCE_NUMBERS) {
+                /*
+                 * No need to check if a value has been set concurrently because Names.createMemberName(…)
+                 * already checked if an equal instance exists in the current JVM.
+                 */
+                SEQUENCE_NUMBERS[localPart] = name;
+            }
+        }
+        return name;
+    }
+
+    /**
      * Returns the Java class associated to the given type name.
      * The method performs the following choices:
      *
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index cc8bce8..0e77f72 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -27,6 +27,7 @@ import java.util.Locale;
 import java.util.Objects;
 import java.io.Serializable;
 import javax.measure.Unit;
+import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
@@ -35,7 +36,7 @@ import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.iso.Types;
+import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.Numbers;
 
 
@@ -78,13 +79,13 @@ public class SampleDimension implements Serializable {
     private static final long serialVersionUID = 6026936545776852758L;
 
     /**
-     * Description for this sample dimension. Typically used as a way to perform a band select by
+     * Identification for this sample dimension. Typically used as a way to perform a band select by
      * using human comprehensible descriptions instead of just numbers. Web Coverage Service (WCS)
      * can use this name in order to perform band sub-setting as directed from a user request.
      *
      * @see #getName()
      */
-    private final InternationalString name;
+    private final GenericName name;
 
     /**
      * The background value, or {@code null} if unspecified. Should be a sample value of
@@ -160,11 +161,11 @@ public class SampleDimension implements Serializable {
      *
      * <p>Note that {@link Builder} provides a more convenient way to create sample dimensions.</p>
      *
-     * @param name        the sample dimension title or description.
+     * @param name        an identification for the sample dimension.
      * @param background  the background value, or {@code null} if none.
      * @param categories  the list of categories. May be empty if none.
      */
-    public SampleDimension(final InternationalString name, final Number background, final Collection<? extends Category> categories) {
+    public SampleDimension(final GenericName name, final Number background, final Collection<? extends Category> categories) {
         ArgumentChecks.ensureNonNull("name", name);
         ArgumentChecks.ensureNonNull("categories", categories);
         final CategoryList list;
@@ -202,15 +203,15 @@ public class SampleDimension implements Serializable {
     }
 
     /**
-     * Returns a name or description for this sample dimension. This is typically used as a way to perform a band select
+     * Returns an identification for this sample dimension. This is typically used as a way to perform a band select
      * by using human comprehensible descriptions instead of just numbers. Web Coverage Service (WCS) can use this name
      * in order to perform band sub-setting as directed from a user request.
      *
-     * @return the title or description of this sample dimension.
+     * @return an identification of this sample dimension.
      *
      * @see org.opengis.metadata.content.RangeDimension#getSequenceIdentifier()
      */
-    public InternationalString getName() {
+    public GenericName getName() {
         return name;
     }
 
@@ -491,9 +492,9 @@ public class SampleDimension implements Serializable {
      */
     public static class Builder {
         /**
-         * Description for this sample dimension.
+         * Identification for this sample dimension.
          */
-        private CharSequence dimensionName;
+        private GenericName dimensionName;
 
         /**
          * The background value, or {@code null} if unspecified.
@@ -521,32 +522,52 @@ public class SampleDimension implements Serializable {
         }
 
         /**
-         * Sets the name or description of the sample dimension.
+         * Sets an identification of the sample dimension.
          * This is the value to be returned by {@link SampleDimension#getName()}.
          * If this method is invoked more than once, then the last specified name prevails
          * (previous sample dimension names are discarded).
          *
-         * @param  name the name or description of the sample dimension.
+         * @param  name  identification of the sample dimension.
          * @return {@code this}, for method call chaining.
          */
-        public Builder setName(final CharSequence name) {
+        public Builder setName(final GenericName name) {
             dimensionName = name;
             return this;
         }
 
         /**
-         * Sets the name of the sample dimension as a band number.
+         * Sets an identification of the sample dimension as a character sequence.
+         * This is a convenience method for creating a {@link GenericName} from the given characters.
+         *
+         * @param  name  identification of the sample dimension.
+         * @return {@code this}, for method call chaining.
+         */
+        public Builder setName(final CharSequence name) {
+            dimensionName = createLocalName(name);
+            return this;
+        }
+
+        /**
+         * Sets an identification of the sample dimension as a band number.
          * This method should be used only when no more descriptive name is available.
          *
          * @param  band  sequence identifier of the sample dimension to create.
          * @return {@code this}, for method call chaining.
          */
         public Builder setName(final int band) {
-            dimensionName = Vocabulary.formatInternational(Vocabulary.Keys.Band_1, band);
+            dimensionName = Names.createMemberName(null, null, band);
             return this;
         }
 
         /**
+         * A common place where are created local names from character string.
+         * For making easier to revisit if we want to add a namespace.
+         */
+        private static GenericName createLocalName(final CharSequence name) {
+            return Names.createLocalName(null, null, name);
+        }
+
+        /**
          * Creates a range for the given minimum and maximum values. We use the static factory methods instead
          * than the {@link NumberRange} constructor for sharing existing range instances. This is also a way
          * to ensure that the number type is one of the primitive wrappers.
@@ -851,15 +872,15 @@ public class SampleDimension implements Serializable {
          * @return the sample dimension.
          */
         public SampleDimension build() {
-            InternationalString name = Types.toInternationalString(dimensionName);
+            GenericName name = dimensionName;
 defName:    if (name == null) {
                 for (final Category category : categories) {
                     if (category.isQuantitative()) {
-                        name = category.name;
+                        name = createLocalName(category.name);
                         break defName;
                     }
                 }
-                name = Vocabulary.formatInternational(Vocabulary.Keys.Untitled);
+                name = createLocalName(Vocabulary.formatInternational(Vocabulary.Keys.Untitled));
             }
             return new SampleDimension(name, background, categories);
         }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
index 39d9cb8..8999af1 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
@@ -21,7 +21,7 @@ import java.util.Locale;
 import java.text.NumberFormat;
 import java.io.IOException;
 import java.text.DecimalFormat;
-import org.opengis.util.InternationalString;
+import org.opengis.util.GenericName;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.measure.Range;
 import org.apache.sis.measure.RangeFormat;
@@ -180,7 +180,7 @@ final class SampleRangeFormat extends RangeFormat {
      * @param title       caption for the table.
      * @param categories  the list of categories to format.
      */
-    final String format(final InternationalString title, final CategoryList categories) {
+    final String format(final GenericName title, final CategoryList categories) {
         final StringBuilder buffer = new StringBuilder(800);
         try {
             format(title, categories, buffer);
@@ -209,10 +209,10 @@ final class SampleRangeFormat extends RangeFormat {
      * @param categories  the list of categories to format.
      * @param out         where to write the category table.
      */
-    void format(final InternationalString title, final CategoryList categories, final Appendable out) throws IOException {
+    void format(final GenericName title, final CategoryList categories, final Appendable out) throws IOException {
         prepare(categories);
         final String lineSeparator = System.lineSeparator();
-        out.append(title.toString(getLocale())).append(lineSeparator);
+        out.append(title.toInternationalString().toString(getLocale())).append(lineSeparator);
         /*
          * Write table header: │ Values │ Measures │ name │
          */
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
new file mode 100644
index 0000000..300de53
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -0,0 +1,128 @@
+/*
+ * 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.coverage.grid;
+
+import java.util.List;
+import java.util.Collection;
+import java.awt.image.RenderedImage;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * Base class of coverages with domains defined as a set of grid points.
+ * The essential property of coverage is to be able to generate a value for any point within its domain.
+ * Since a grid coverage is represented by a grid of values, the value returned by the coverage for a point
+ * is that of the grid value whose location is nearest the point.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public abstract class GridCoverage {
+    /**
+     * The grid extent, coordinate reference system (CRS) and conversion from cell indices to CRS.
+     */
+    private final GridGeometry gridGeometry;
+
+    /**
+     * List of sample dimension (band) information for the grid coverage. Information include such things
+     * as description, the no data values, minimum and maximum values, <i>etc</i>. A coverage must have
+     * at least one sample dimension. The content of this array shall never be modified.
+     */
+    private final SampleDimension[] sampleDimensions;
+
+    /**
+     * Constructs a grid coverage using the specified grid geometry and sample dimensions.
+     *
+     * @param grid   the grid extent, CRS and conversion from cell indices to CRS.
+     * @param bands  sample dimensions for each image band.
+     */
+    protected GridCoverage(final GridGeometry grid, final Collection<? extends SampleDimension> bands) {
+        ArgumentChecks.ensureNonNull("grid",  grid);
+        ArgumentChecks.ensureNonNull("bands", bands);
+        gridGeometry = grid;
+        sampleDimensions = bands.toArray(new SampleDimension[bands.size()]);
+        for (int i=0; i<sampleDimensions.length; i++) {
+            ArgumentChecks.ensureNonNullElement("bands", i, sampleDimensions[i]);
+        }
+    }
+
+    /**
+     * Returns the coordinate reference system to which the values in grid domain are referenced.
+     * This is the CRS used when accessing a coverage with the {@code evaluate(…)} methods.
+     * This coordinate reference system is usually different than the coordinate system of the grid.
+     * It is the target coordinate reference system of the {@link GridGeometry#getGridToCRS gridToCRS}
+     * math transform.
+     *
+     * <p>The default implementation delegates to {@link GridGeometry#getCoordinateReferenceSystem()}.</p>
+     *
+     * @return the CRS used when accessing a coverage with the {@code evaluate(…)} methods.
+     * @throws IncompleteGridGeometryException if the grid geometry has no CRS.
+     */
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return gridGeometry.getCoordinateReferenceSystem();
+    }
+
+    /**
+     * Returns information about the <cite>domain</cite> of this grid coverage.
+     * Information includes the grid extent, CRS and conversion from cell indices to CRS.
+     * {@code GridGeometry} can also provide derived information like bounding box and resolution.
+     *
+     * @return grid extent, CRS and conversion from cell indices to CRS.
+     */
+    public GridGeometry getGridGeometry() {
+        return gridGeometry;
+    }
+
+    /**
+     * Returns information about the <cite>range</cite> of this grid coverage.
+     * Information include names, sample value ranges, fill values and transfer functions for all bands in this grid coverage.
+     *
+     * @return names, value ranges, fill values and transfer functions for all bands in this grid coverage.
+     */
+    public List<SampleDimension> getSampleDimensions() {
+        return UnmodifiableArrayList.wrap(sampleDimensions);
+    }
+
+    /**
+     * Returns a two-dimensional slice of grid data as a rendered image.
+     * This method tries to return a view as much as possible (i.e. sample values are not copied).
+     *
+     * @param  xAxis  dimension to use for <var>x</var> axis.
+     * @param  yAxis  dimension to use for <var>y</var> axis.
+     * @return the grid data as a rendered image in the given CRS dimensions.
+     */
+    public abstract RenderedImage asRenderedImage(int xAxis, int yAxis);
+
+    /**
+     * Returns a string representation of this grid coverage for debugging purpose.
+     *
+     * @return a string representation of this grid coverage for debugging purpose.
+     */
+    @Override
+    public String toString() {
+        final String lineSeparator = System.lineSeparator();
+        final StringBuilder buffer = new StringBuilder(1000);
+        buffer.append("Grid coverage domain:").append(lineSeparator);
+        gridGeometry.formatTo(buffer, lineSeparator + "  ");
+        return buffer.toString();
+    }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 9f10131..aa8a5ca 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -903,7 +903,15 @@ public class GridGeometry implements Serializable {
             throw new IllegalArgumentException(Errors.format(
                     Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
         }
-        return new Formatter(bitmask).toString();
+        return new Formatter(new StringBuilder(512), bitmask, System.lineSeparator()).toString();
+    }
+
+    /**
+     * Formats a string representation of this grid geometry in the specified buffer.
+     * This is used for reducing the amount of copies in {@link GridCoverage#toString()}.
+     */
+    final void formatTo(final StringBuilder out, final String lineSeparator) {
+        new Formatter(out, EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION, lineSeparator).format();
     }
 
     /**
@@ -950,14 +958,14 @@ public class GridGeometry implements Serializable {
          * Creates a new formatter for the given combination of {@link #EXTENT}, {@link #ENVELOPE},
          * {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
          */
-        Formatter(final int bitmask) {
-            this.bitmask  = bitmask;
-            lineSeparator = System.lineSeparator();
-            locale        = Locale.getDefault(Locale.Category.DISPLAY);
-            vocabulary    = Vocabulary.getResources(locale);
-            buffer        = new StringBuilder(512);
-            crs           = (envelope != null) ? envelope.getCoordinateReferenceSystem() : null;
-            cs            = (crs != null) ? crs.getCoordinateSystem() : null;
+        Formatter(final StringBuilder out, final int bitmask, final String lineSeparator) {
+            this.buffer        = out;
+            this.bitmask       = bitmask;
+            this.lineSeparator = lineSeparator;
+            this.locale        = Locale.getDefault(Locale.Category.DISPLAY);
+            this.vocabulary    = Vocabulary.getResources(locale);
+            this.crs           = (envelope != null) ? envelope.getCoordinateReferenceSystem() : null;
+            this.cs            = (crs != null) ? crs.getCoordinateSystem() : null;
         }
 
         /**
@@ -965,6 +973,15 @@ public class GridGeometry implements Serializable {
          */
         @Override
         public final String toString() {
+            format();
+            return buffer.toString();
+        }
+
+        /**
+         * Formats a string representation of the enclosing {@link GridGeometry} instance
+         * in the buffer specified at construction time.
+         */
+        final void format() {
             /*
              * Example: Grid extent
              * ├─ Dimension 0: [370 … 389]  (20 cells)
@@ -1056,7 +1073,6 @@ public class GridGeometry implements Serializable {
                     buffer.append(lineSeparator);
                 }
             }
-            return buffer.toString();
         }
 
         /**
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
new file mode 100644
index 0000000..f03de49
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
@@ -0,0 +1,58 @@
+/*
+ * 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.raster;
+
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.IndexColorModel;
+
+
+/**
+ * Creates color models from given properties.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final class ColorModelFactory {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private ColorModelFactory() {
+    }
+
+    /**
+     * Creates for image of the given type.
+     *
+     * @param  type  one of {@link DataBuffer} constant.
+     * @return color model.
+     *
+     * @todo need much improvement.
+     */
+    public static ColorModel create(final int type) {
+        final int bits = DataBuffer.getDataTypeSize(type);
+        final int[] ARGB = new int[1 << bits];
+        final float scale = 255f / ARGB.length;
+        for (int i=0; i<ARGB.length; i++) {
+            int c = Math.round(scale * i);
+            c |= (c << 8) | (c << 16);
+            ARGB[i] = c;
+        }
+        return new IndexColorModel(bits, ARGB.length, ARGB, 0, false, -1, DataBuffer.TYPE_USHORT);
+    }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
new file mode 100644
index 0000000..009c1a5
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
@@ -0,0 +1,83 @@
+/*
+ * 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.raster;
+
+import java.awt.Point;
+import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.WritableRaster;
+import org.apache.sis.util.Static;
+
+
+/**
+ * Creates rasters from given properties.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final class RasterFactory extends Static {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private RasterFactory() {
+    }
+
+    /**
+     * Wraps the given data buffer in a raster.
+     *
+     * @param  buffer          buffer that contains the sample values.
+     * @param  width           raster width in pixels.
+     * @param  height          raster height in pixels.
+     * @param  scanlineStride  line stride of raster data.
+     * @param  bankIndices     bank indices for each band, or {@code null} for 0, 1, 2, 3….
+     * @param  bandOffsets     offsets of all bands, or {@code null} for all 0.
+     * @param  location        the upper-left corner of the raster, or {@code null} for (0,0).
+     * @return a raster built from given properties.
+     *
+     * @see WritableRaster#createBandedRaster(DataBuffer, int, int, int, int[], int[], Point)
+     */
+    public static WritableRaster createBandedRaster​(final DataBuffer buffer,
+            final int width, final int height, final int scanlineStride,
+            int[] bankIndices, int[] bandOffsets, final Point location)
+    {
+        if (bankIndices == null) {
+            bankIndices = new int[buffer.getNumBanks()];
+            for (int i=1; i<bankIndices.length; i++) {
+                bankIndices[i] = i;
+            }
+        }
+        if (bandOffsets == null) {
+            bandOffsets = new int[bankIndices.length];
+        }
+        final int dataType = buffer.getDataType();
+        switch (dataType) {
+            case DataBuffer.TYPE_BYTE:
+            case DataBuffer.TYPE_USHORT:
+            case DataBuffer.TYPE_INT: {
+                // This constructor supports only above-cited types.
+                return WritableRaster.createBandedRaster(buffer, width, width, scanlineStride, bankIndices, bandOffsets, location);
+            }
+            default: {
+                SampleModel model = new BandedSampleModel(dataType, width, height, scanlineStride, bankIndices, bandOffsets);
+                return WritableRaster.createWritableRaster(model, buffer, location);
+            }
+        }
+    }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
index 8f20a5d..d05a107 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
@@ -16,6 +16,13 @@
  */
 package org.apache.sis.internal.jdk9;
 
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.DoubleBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.LongBuffer;
+import java.nio.ShortBuffer;
 import java.util.Arrays;
 import java.util.Set;
 import java.util.List;
@@ -29,7 +36,7 @@ import org.apache.sis.internal.util.UnmodifiableArrayList;
  * This file will be deleted on the SIS JDK9 branch.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @since   0.8
+ * @since   1.0
  * @version 0.8
  * @module
  */
@@ -71,4 +78,20 @@ public final class JDK9 {
             default: return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(elements)));
         }
     }
+
+    /**
+     * Place holder for {@code Buffer.slice()}.
+     *
+     * @param  b the buffer to slice.
+     * @return the sliced buffer.
+     */
+    public static Buffer slice(Buffer b) {
+        if (b instanceof ByteBuffer)   return ((ByteBuffer) b).slice();
+        if (b instanceof ShortBuffer)  return ((ShortBuffer) b).slice();
+        if (b instanceof IntBuffer)    return ((IntBuffer) b).slice();
+        if (b instanceof LongBuffer)   return ((LongBuffer) b).slice();
+        if (b instanceof FloatBuffer)  return ((FloatBuffer) b).slice();
+        if (b instanceof DoubleBuffer) return ((DoubleBuffer) b).slice();
+        throw new IllegalArgumentException();
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java b/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
index 8936dc9..f6a4a70 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
@@ -17,6 +17,13 @@
 package org.apache.sis.math;
 
 import java.io.Serializable;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.IntBuffer;
+import java.nio.LongBuffer;
+import java.nio.FloatBuffer;
+import java.nio.DoubleBuffer;
 import java.util.Arrays;
 import java.util.function.IntSupplier;
 import org.apache.sis.util.Numbers;
@@ -85,6 +92,7 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
      * or {@code null} if this method can not do better than the given {@code Vector} instance.
      * This method shall be invoked only for vector of integer values (this is not verified).
      */
+    @SuppressWarnings("null")
     static Vector compress(final Vector source, final long min, final long max) {
         boolean isSigned = (min >= Byte.MIN_VALUE && max <= Byte.MAX_VALUE);
         if (isSigned || (min >= 0 && max <= 0xFF)) {
@@ -279,6 +287,11 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
             return NumberRange.create(min, true, max, true);
         }
 
+        /** Wraps this vector in a buffer. */
+        @Override public Buffer buffer() {
+            return DoubleBuffer.wrap(array);
+        }
+
         /** Returns a copy of current data as a floating point array. */
         @Override public double[] doubleValues() {
             return array.clone();
@@ -389,6 +402,11 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
             return NumberRange.create(min, true, max, true);
         }
 
+        /** Wraps this vector in a buffer. */
+        @Override public final Buffer buffer() {
+            return FloatBuffer.wrap(array);
+        }
+
         /** Returns a copy of current data as a floating point array. */
         @Override public final float[] floatValues() {
             return array.clone();
@@ -548,6 +566,11 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
             return null;
         }
 
+        /** Wraps this vector in a buffer. */
+        @Override public final Buffer buffer() {
+            return LongBuffer.wrap(array);
+        }
+
         /** Applies hash code contract specified {@link Vector#hashCode()}. */
         @Override public final int hashCode() {
             return Arrays.hashCode(array);
@@ -665,6 +688,11 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
             return null;
         }
 
+        /** Wraps this vector in a buffer. */
+        @Override public final Buffer buffer() {
+            return IntBuffer.wrap(array);
+        }
+
         /** Applies hash code contract specified {@link Vector#hashCode()}. */
         @Override public final int hashCode() {
             return Arrays.hashCode(array);
@@ -757,6 +785,11 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
          * performed by Java would make the implementation a little bit more tricky.
          */
 
+        /** Wraps this vector in a buffer. */
+        @Override public final Buffer buffer() {
+            return ShortBuffer.wrap(array);
+        }
+
         /** Applies hash code contract specified {@link Vector#hashCode()}. */
         @Override public final int hashCode() {
             return Arrays.hashCode(array);
@@ -850,6 +883,11 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
          * performed by Java would make the implementation a little bit more tricky.
          */
 
+        /** Wraps this vector in a buffer. */
+        @Override public final Buffer buffer() {
+            return ByteBuffer.wrap(array);
+        }
+
         /** Applies hash code contract specified {@link Vector#hashCode()}. */
         @Override public final int hashCode() {
             return Arrays.hashCode(array);
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
index 8a4af37..fc384a6 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
@@ -17,11 +17,13 @@
 package org.apache.sis.math;
 
 import java.io.Serializable;
+import java.nio.Buffer;
 import java.util.Arrays;
 import java.util.AbstractList;
 import java.util.RandomAccess;
 import java.util.StringJoiner;
 import java.util.function.IntSupplier;
+import org.apache.sis.internal.jdk9.JDK9;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.ArraysExt;
@@ -941,6 +943,17 @@ search:     for (;;) {
             final Vector c = super.compress(tolerance);
             return (c != this) ? c : copy();
         }
+
+        /**
+         * Returns a buffer over the sub-section represented by this {@code SubSampling} instance.
+         */
+        @Override
+        public Buffer buffer() {
+            if (step == 1) {
+                return JDK9.slice(Vector.this.buffer().position(first).limit(first + length));
+            }
+            return super.buffer();
+        }
     }
 
     /**
@@ -1257,6 +1270,21 @@ search:     for (;;) {
     }
 
     /**
+     * Returns the vector data as a {@code java.nio} buffer.
+     * Data are not copied: changes in the buffer are reflected on this vector and vice-versa.
+     * Date are provided in their "raw" form. For example unsigned integers are given as plain {@code int} elements
+     * and it is caller responsibility to use {@link Integer#toUnsignedLong(int)} if needed.
+     *
+     * @return the vector data as a buffer.
+     * @throws UnsupportedOperationException if this vector can not be represented by a buffer.
+     *
+     * @since 1.0
+     */
+    public Buffer buffer() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
      * Copies all values in an array of double precision floating point numbers.
      * This method is for inter-operability with APIs requiring an array of primitive type.
      *
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 022d5c2..5beda8d 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
@@ -37,6 +37,7 @@ import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.SampleDimension;
@@ -1278,6 +1279,19 @@ final class ImageFileDirectory extends AbstractGridResource {
     }
 
     /**
+     * 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 index of sample dimensions to read, or an empty sequence for reading all ranges.
+     * @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(final GridGeometry domain, final int... range) throws DataStoreException {
+        throw new DataStoreException("Not yet implemented.");   // TODO
+    }
+
+    /**
      * Reports a warning with a message created from the given resource keys and parameters.
      *
      * @param  level       the logging level for the message to log.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
index b86a8cc..8b6a45b 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
@@ -16,8 +16,17 @@
  */
 package org.apache.sis.internal.netcdf;
 
+import java.nio.Buffer;
 import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferShort;
+import java.awt.image.DataBufferUShort;
+import java.awt.image.DataBufferInt;
+import java.awt.image.DataBufferFloat;
+import java.awt.image.DataBufferDouble;
 import org.apache.sis.util.Numbers;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.storage.DataStoreContentException;
 
 
 /**
@@ -190,6 +199,30 @@ public enum DataType {
     }
 
     /**
+     * Creates a Java2D buffer for the given {@code java.nio} buffer.
+     *
+     * @param  data  an array of primitive type such as {@code byte[]} or {@code float[]}.
+     * @return Java2D data buffer wrapping the given primitive array.
+     * @throws UnsupportedOperationException if the buffer is not backed by an array.
+     * @throws DataStoreContentException if this enumeration is not a supported type for this operation.
+     * @throws ClassCastException if the given array is not of the type expected by this enumeration value.
+     */
+    public DataBuffer toJava2D(final Buffer data) throws DataStoreContentException {
+        final Object array = data.array();
+        final int offset = data.arrayOffset() + data.position();
+        final int length = data.remaining();
+        switch (rasterDataType) {
+            case DataBuffer.TYPE_BYTE:   return new DataBufferByte  (  (byte[]) array, length, offset);
+            case DataBuffer.TYPE_SHORT:  return new DataBufferShort ( (short[]) array, length, offset);
+            case DataBuffer.TYPE_USHORT: return new DataBufferUShort( (short[]) array, length, offset);
+            case DataBuffer.TYPE_INT:    return new DataBufferInt   (   (int[]) array, length, offset);
+            case DataBuffer.TYPE_FLOAT:  return new DataBufferFloat ( (float[]) array, length, offset);
+            case DataBuffer.TYPE_DOUBLE: return new DataBufferDouble((double[]) array, length, offset);
+            default: throw new DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, name()));
+        }
+    }
+
+    /**
      * An array of all supported netCDF data types ordered in such a way that
      * {@code VALUES[codeNetCDF - 1]} is the enumeration value for a given netCDF code.
      */
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
index 6b1ba58..4c69e52 100644
--- 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
@@ -32,11 +32,15 @@ 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.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.math.Vector;
 import ucar.nc2.constants.CDM;                      // We use only String constants.
 
 
@@ -222,6 +226,56 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
     }
 
     /**
+     * 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 index of sample dimensions to read, or an empty sequence for reading all ranges.
+     * @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, final int... range) throws DataStoreException {
+        if (range != null && range.length != 0) {
+            throw new DataStoreException("Unsupported range subsetting.");      // TODO
+        }
+        if (domain != null) {
+            throw new DataStoreException("Unsupported domain subsetting.");      // TODO
+        }
+        domain = getGridGeometry();
+        final GridExtent extent = domain.getExtent();
+        final int   dimension   = domain.getDimension();
+        final int[] areaLower   = new int[dimension];
+        final int[] areaUpper   = new int[dimension];
+        final int[] subsampling = new int[dimension];
+        for (int i=0; i<dimension; i++) {
+            final int j = (dimension - 1) - i;
+            areaLower[j] = unsigned(extent.getLow (i));             // Inclusive.
+            areaUpper[j] = unsigned(extent.getHigh(i) + 1);         // Exclusive.
+            if (i >= 2) {
+                areaUpper[j] = areaLower[j] + 1;                    // TODO
+            }
+            subsampling[j] = 1;
+        }
+        final Vector samples;
+        try {
+            samples = data.read(areaLower, areaUpper, subsampling);
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+        return new Image(domain, getSampleDimensions(), data.getDataType().toJava2D(samples.buffer()));
+    }
+
+    /**
+     * Returns the given value as an unsigned integer.
+     */
+    private static int unsigned(final long value) throws DataStoreException {
+        if (value < 0L || value > 0xFFFFFFFFL) {
+            throw new DataStoreException(Errors.format(Errors.Keys.IndexOutOfBounds_1, value));
+        }
+        return (int) value;
+    }
+
+    /**
      * Gets the paths to files used by this resource, or an empty array if unknown.
      */
     @Override
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Image.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Image.java
new file mode 100644
index 0000000..027734d
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Image.java
@@ -0,0 +1,75 @@
+/*
+ * 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.awt.image.DataBuffer;
+import java.awt.image.ColorModel;
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.internal.raster.RasterFactory;
+import org.apache.sis.internal.raster.ColorModelFactory;
+
+
+/**
+ * Data loaded from a {@link GridResource}.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class Image extends GridCoverage {
+    /**
+     * The sample values.
+     */
+    private final DataBuffer data;
+
+    /**
+     * Creates a new raster from the given resource.
+     */
+    Image(final GridGeometry domain, final List<SampleDimension> range, final DataBuffer data) {
+        super(domain, range);
+        this.data = data;
+    }
+
+    /**
+     * Returns a two-dimensional slice of grid data as a rendered image.
+     * This method tries to return a view as much as possible (i.e. sample values are not copied).
+     *
+     * @param  xAxis  dimension to use for <var>x</var> axis.
+     * @param  yAxis  dimension to use for <var>y</var> axis.
+     * @return the grid data as a rendered image in the given CRS dimensions.
+     */
+    @Override
+    public RenderedImage asRenderedImage(final int xAxis, final int yAxis) {
+        if (xAxis != 0 || yAxis != 1) {
+            throw new IllegalArgumentException();       // TODO
+        }
+        final GridExtent extent = getGridGeometry().getExtent();
+        final int width  = Math.toIntExact(extent.getSize(xAxis));
+        final int height = Math.toIntExact(extent.getSize(yAxis));
+        final WritableRaster raster = RasterFactory.createBandedRaster(data, width, height, width, null, null, null);
+        final ColorModel colors = ColorModelFactory.create(data.getDataType());
+        return new BufferedImage(colors, raster, false, null);
+    }
+}
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 5590872..bd06abb 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
@@ -16,13 +16,14 @@
  */
 package org.apache.sis.internal.storage;
 
+import org.opengis.geometry.Envelope;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.util.logging.WarningListeners;
-import org.opengis.geometry.Envelope;
 
 
 /**
@@ -81,5 +82,8 @@ public abstract class AbstractGridResource extends AbstractResource implements G
     protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException {
         super.createMetadata(metadata);
         metadata.addSpatialRepresentation(null, getGridGeometry(), false);
+        for (final SampleDimension band : getSampleDimensions()) {
+            metadata.addNewBand(band);
+        }
     }
 }
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 2ff8150..26b8e8c 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
@@ -119,6 +119,7 @@ import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.metadata.sql.MetadataSource;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.iso.Names;
@@ -148,11 +149,6 @@ import org.opengis.metadata.citation.Responsibility;
  */
 public class MetadataBuilder {
     /**
-     * Band numbers, created when first needed.
-     */
-    private static final MemberName[] BAND_NUMBERS = new MemberName[16];
-
-    /**
      * Whether the next party to create should be an instance of {@link DefaultIndividual} or {@link DefaultOrganisation}.
      *
      * @see #party()
@@ -1896,7 +1892,7 @@ parse:      for (int i = 0; i < length;) {
 
     /**
      * Adds and populates a "spatial representation info" node using the given grid geometry.
-     * If this method invokes implicitly {@link #newGridRepresentation(GridType)}, unless this
+     * This method invokes implicitly {@link #newGridRepresentation(GridType)}, unless this
      * method returns {@code false} in which case nothing has been done.
      * Storage location is:
      *
@@ -2231,6 +2227,49 @@ parse:      for (int i = 0; i < length;) {
     }
 
     /**
+     * Sets the sequence identifier, sample value ranges, transfer function and units of measurement
+     * from the given sample dimension. This method dispatch its work to other methods in this class.
+     * Before to set any value, this method starts a new band by calling {@link #newSampleDimension()}.
+     * Storage locations are:
+     *
+     * <ul>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/sequenceIdentifier}</li>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/minValue}</li>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/maxValue}</li>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/scale}</li>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/offset}</li>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/transferFunctionType}</li>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/unit}</li>
+     * </ul>
+     *
+     * @param  band  the sample dimension to describe in metadata, or {@code null} if none.
+     */
+    public final void addNewBand(final SampleDimension band) {
+        if (band != null) {
+            newSampleDimension();
+            final GenericName g = band.getName();
+            if (g != null) {
+                final MemberName name;
+                if (g instanceof MemberName) {
+                    name = (MemberName) g;
+                } else {
+                    name = Names.createMemberName(null, null, g.tip().toString(), String.class);
+                }
+                setBandIdentifier(name);
+            }
+            band.getSampleRange().ifPresent((range) -> {
+                addMinimumSampleValue(range.getMinDouble(true));
+                addMaximumSampleValue(range.getMaxDouble(true));
+            });
+            band.getTransferFunctionFormula().ifPresent((tr) -> {
+                setTransferFunction(tr.getScale(), tr.getOffset());
+                sampleDimension().setTransferFunctionType(tr.getType());
+            });
+            band.getUnits().ifPresent((unit) -> setSampleUnits(unit));
+        }
+    }
+
+    /**
      * Sets the name or number that uniquely identifies instances of bands of wavelengths on which a sensor operates.
      * If a coverage contains more than one band, additional bands can be created by calling
      * {@link #newSampleDimension()} before to call this method.
@@ -2257,28 +2296,13 @@ parse:      for (int i = 0; i < length;) {
      */
     public final void setBandIdentifier(final int sequenceIdentifier) {
         if (sequenceIdentifier > 0) {
-            final boolean cached = (sequenceIdentifier <= BAND_NUMBERS.length);
-            MemberName name = null;
-            if (cached) synchronized (BAND_NUMBERS) {
-                name = BAND_NUMBERS[sequenceIdentifier - 1];
-            }
-            if (name == null) {
-                name = Names.createMemberName(null, null, String.valueOf(sequenceIdentifier), Integer.class);
-                if (cached) synchronized (BAND_NUMBERS) {
-                    /*
-                     * No need to check if a value has been set concurrently because Names.createMemberName(…)
-                     * already checked if an equal instance exists in the current JVM.
-                     */
-                    BAND_NUMBERS[sequenceIdentifier - 1] = name;
-                }
-            }
-            setBandIdentifier(name);
+            setBandIdentifier(Names.createMemberName(null, null, sequenceIdentifier));
         }
     }
 
     /**
      * Adds an identifier for the current band.
-     * These identifiers can be use to provide names for the attribute from a standard set of names.
+     * These identifiers can be used to provide names for the attribute from a standard set of names.
      * If a coverage contains more than one band, additional bands can be created by calling
      * {@link #newSampleDimension()} before to call this method.
      * Storage location is:
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 a6e10ec..6e9bd6e 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
@@ -19,6 +19,7 @@ package org.apache.sis.storage;
 import java.util.List;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridCoverage;
 
 
 /**
@@ -71,4 +72,21 @@ public interface GridCoverageResource extends DataSet {
      * @throws DataStoreException if an error occurred while reading definitions from the underlying data store.
      */
     List<SampleDimension> getSampleDimensions() throws DataStoreException;
+
+    /**
+     * Loads a subset of the grid coverage represented by this resource. If a non-null grid geometry is specified,
+     * then this method will try to return a grid coverage matching the given grid geometry on a best-effort basis;
+     * the coverage actually returned may have a different resolution, cover a different area in a different CRS,
+     * <i>etc</i>. The general contract is that the returned coverage should not contain less data than a coverage
+     * matching exactly the given geometry.
+     *
+     * <p>While this method name suggests an immediate reading, some implementations may defer the actual reading
+     * at a later stage.</p>
+     *
+     * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
+     * @param  range   0-based index of sample dimensions to read, or an empty sequence for reading all ranges.
+     * @return the grid coverage for the specified domain and range.
+     * @throws DataStoreException if an error occurred while reading the grid coverage data.
+     */
+    GridCoverage read(GridGeometry domain, int... range) throws DataStoreException;
 }


[sis] 01/02: Reimplement GeoTiffStore.components() as a List instead than a Set.

Posted by de...@apache.org.
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 53282903a63a4ac2bdd6684a25e7a685840ed9e0
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Dec 9 14:09:48 2018 +0100

    Reimplement GeoTiffStore.components() as a List instead than a Set.
---
 .../main/java/org/apache/sis/coverage/ToNaN.java   |   8 +-
 .../sis/internal/util/ListOfUnknownSize.java       | 263 +++++++++++++++++++++
 .../apache/sis/internal/util/SetOfUnknownSize.java |  24 +-
 .../sis/internal/util/ListOfUnknownSizeTest.java   |  60 +++++
 .../apache/sis/test/suite/UtilityTestSuite.java    |   1 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  93 ++++----
 6 files changed, 399 insertions(+), 50 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
index 4a3fc3e..ab49274 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
@@ -59,8 +59,12 @@ final class ToNaN extends HashSet<Integer> implements DoubleToIntFunction {
     }
 
     /**
-     * Returns a NaN ordinal value for the given sample value.
-     * The returned value can be given to {@link MathFunctions#toNanFloat(int)}.
+     * Mapping from sample values to ordinal values to be supplied to {@link MathFunctions#toNanFloat(int)}.
+     * That mapping shall ensure that there is no ordinal value collision between different categories in
+     * the same {@link SampleDimension}.
+     *
+     * @param  value  a real number in the {@link Category#range} sample value range.
+     * @return a value between {@value MathFunctions#MIN_NAN_ORDINAL} and {@value MathFunctions#MAX_NAN_ORDINAL} inclusive.
      */
     @Override
     public int applyAsInt(final double value) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
new file mode 100644
index 0000000..3849c6e
--- /dev/null
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
@@ -0,0 +1,263 @@
+/*
+ * 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.util;
+
+import java.util.List;
+import java.util.AbstractSequentialList;
+import java.util.Collection;
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.Objects;
+
+
+/**
+ * An alternative to {@code AbstractList} for implementations having a costly {@link #size()} method.
+ * This class overrides some methods in a way that avoid or reduce calls to {@link #size()}.
+ *
+ * <p>Despite extending {@link AbstractSequentialList}, this class expects implementations to override
+ * the random access method {@link #get(int)} instead than {@link #listIterator(int)}.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ *
+ * @param <E>  the type of elements in the list.
+ *
+ * @since 1.0
+ * @module
+ */
+public abstract class ListOfUnknownSize<E> extends AbstractSequentialList<E> {
+    /**
+     * For subclass constructors.
+     */
+    protected ListOfUnknownSize() {
+    }
+
+    /**
+     * Returns {@link #size()} if its value is already known, or -1 if the size is still unknown.
+     * The size may become known for example if it has been cached by the subclass. In such case,
+     * some {@code ListOfUnknownSize} methods will take a more efficient path.
+     *
+     * @return {@link #size()} if its value is already known, or -1 if it still costly to compute.
+     */
+    protected int sizeIfKnown() {
+        return -1;
+    }
+
+    /**
+     * Returns the number of elements in this list. The default implementation counts the number of elements
+     * for which {@link #exists(int)} returns {@code true}. Subclasses are encouraged to cache this value if
+     * they know that the underlying storage is immutable.
+     *
+     * @return the number of elements in this list.
+     */
+    @Override
+    public int size() {
+        int size = sizeIfKnown();
+        if (size < 0) {
+            size = 0;
+            while (exists(size)) {
+                if (++size == Integer.MAX_VALUE) {
+                    break;
+                }
+            }
+        }
+        return size;
+    }
+
+    /**
+     * Returns {@code true} if this list is empty.
+     * This method avoids to invoke {@link #size()} unless it is cheap.
+     *
+     * @return {@code true} if this list is empty.
+     */
+    @Override
+    public boolean isEmpty() {
+        final int size = sizeIfKnown();
+        return (size == 0) || (size < 0 && !exists(0));
+    }
+
+    /**
+     * Returns {@code true} if an element exists at the given index. If an element at index <var>i</var> exists,
+     * then all elements at index 0 to <var>i</var> - 1 also exist. Those elements do not need to be computed
+     * immediately if their computation is deferred.
+     *
+     * @param  index  the index where to verify if an element exists.
+     * @return {@code true} if an element exists at the given index.
+     */
+    protected abstract boolean exists(int index);
+
+    /**
+     * Returns the element at the specified index.
+     * Invoking this method may trig computation of the element if their computation is deferred.
+     *
+     * @param  index  position of the element to get in this list.
+     * @return the element at the given index.
+     * @throws IndexOutOfBoundsException if the given index is out of bounds.
+     */
+    @Override
+    public abstract E get(int index);
+
+    /**
+     * Removes elements of the given collection from this list.
+     * This method avoids to invoke {@link #size()}.
+     *
+     * @param  c  the collection containing elements to remove.
+     * @return {@code true} if at least one element has been removed.
+     */
+    @Override
+    public boolean removeAll(final Collection<?> c) {
+        // See comment in SetOfUnknownSize.removeAll(…).
+        boolean modified = false;
+        for (final java.util.Iterator<?> it = c.iterator(); it.hasNext();) {
+            modified |= remove(it.next());
+        }
+        return modified;
+    }
+
+    /**
+     * Returns a list iterator over the elements in this list.
+     * The default implementation invokes {@link #exists(int)} and {@link #get(int)}.
+     * Write operations are not supported.
+     *
+     * @param  index  index of first element to be returned from the list.
+     * @return a list iterator over the elements in this list.
+     * @throws IndexOutOfBoundsException if the given index is out of bounds.
+     */
+    @Override
+    public ListIterator<E> listIterator(final int index) {
+        return new Iterator(index);
+    }
+
+    /**
+     * The iterator returned by {@link #listIterator()}.  Provided as a named class instead
+     * than anonymous class for more readable stack traces. This is especially useful since
+     * elements may be loaded or computed when first needed, and those operations may fail.
+     */
+    private final class Iterator implements ListIterator<E> {
+        /** Index of the next element to be returned. */
+        private int cursor;
+
+        /** Creates a new iterator starting at the given index. */
+        Iterator(final int index) {
+            cursor = index;
+        }
+
+        /** Index of element to be returned by {@link #next()}. */
+        @Override public int nextIndex() {
+            return cursor;
+        }
+
+        /** Whether {@link #next()} can succeed. */
+        @Override public boolean hasNext() {
+            final int size = sizeIfKnown();
+            return (size >= 0) ? cursor < size : exists(cursor);
+        }
+
+        /** Move forward by one element. */
+        @Override public E next() {
+            final E element;
+            try {
+                element = get(cursor);
+            } catch (IndexOutOfBoundsException e) {
+                throw (NoSuchElementException) new NoSuchElementException().initCause(e);
+            }
+            cursor++;           // Set only on success.
+            return element;
+        }
+
+        /** Index of element to be returned by {@link #previous()}. */
+        @Override public int previousIndex() {
+            return cursor - 1;
+        }
+
+        /** Whether {@link #previous()} can succeed. */
+        @Override public boolean hasPrevious() {
+            return cursor != 0;
+        }
+
+        /** Move backward by one element. */
+        @Override public E previous() {
+            if (cursor != 0) return get(--cursor);
+            throw new NoSuchElementException();
+        }
+
+        @Override public void set(E e) {throw new UnsupportedOperationException();}
+        @Override public void add(E e) {throw new UnsupportedOperationException();}
+        @Override public void remove() {throw new UnsupportedOperationException();}
+    }
+
+    /**
+     * Creates a {@code Spliterator} without knowledge of collection size.
+     *
+     * @return a {@code Spliterator} over the elements in this collection.
+     */
+    @Override
+    public Spliterator<E> spliterator() {
+        return sizeIfKnown() >= 0 ? super.spliterator() : Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED);
+    }
+
+    /**
+     * Returns the elements in an array.
+     *
+     * @return an array containing all list elements.
+     */
+    @Override
+    public Object[] toArray() {
+        return sizeIfKnown() >= 0 ? super.toArray() : SetOfUnknownSize.toArray(iterator(), new Object[32], true);
+    }
+
+    /**
+     * Returns the elements in the given array, or in a new array of the same type
+     * if it was necessary to allocate more space.
+     *
+     * @param  <T>    the type array elements.
+     * @param  array  where to store the elements.
+     * @return an array containing all list elements.
+     */
+    @Override
+    @SuppressWarnings("SuspiciousToArrayCall")
+    public <T> T[] toArray(final T[] array) {
+        return sizeIfKnown() >= 0 ? super.toArray(array) : SetOfUnknownSize.toArray(iterator(), array, false);
+    }
+
+    /**
+     * Returns {@code true} if the given object is also a list and the two lists have the same content.
+     * This method avoids to invoke {@link #size()} on this instance.
+     *
+     * @param  object  the object to compare with this list.
+     * @return {@code true} if the two list have the same content.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object == this) {
+            return true;
+        }
+        if (!(object instanceof List<?>)) {
+            return false;
+        }
+        final java.util.Iterator<E> it = iterator();
+        final java.util.Iterator<?> ot = ((List<?>) object).iterator();
+        while (it.hasNext()) {
+            if (!ot.hasNext() || !Objects.equals(it.next(), ot.next())) {
+                return false;
+            }
+        }
+        return !ot.hasNext();
+    }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java
index 0bf0f9e..be3fcda 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java
@@ -21,6 +21,8 @@ import java.util.AbstractSet;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.Arrays;
+import java.util.Spliterator;
+import java.util.Spliterators;
 import org.apache.sis.util.ArraysExt;
 
 
@@ -29,7 +31,7 @@ import org.apache.sis.util.ArraysExt;
  * This class overrides some methods in a way that avoid or reduce calls to {@link #size()}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @param <E>  the type of elements in the set.
  *
@@ -107,13 +109,23 @@ public abstract class SetOfUnknownSize<E> extends AbstractSet<E> {
     }
 
     /**
+     * Creates a {@code Spliterator} without knowledge of collection size.
+     *
+     * @return a {@code Spliterator} over the elements in this collection.
+     */
+    @Override
+    public Spliterator<E> spliterator() {
+        return isSizeKnown() ? super.spliterator() : Spliterators.spliteratorUnknownSize(iterator(), 0);
+    }
+
+    /**
      * Returns the elements in an array.
      *
      * @return an array containing all set elements.
      */
     @Override
     public Object[] toArray() {
-        return isSizeKnown() ? super.toArray() : toArray(new Object[32], true);
+        return isSizeKnown() ? super.toArray() : toArray(iterator(), new Object[32], true);
     }
 
     /**
@@ -127,16 +139,16 @@ public abstract class SetOfUnknownSize<E> extends AbstractSet<E> {
     @Override
     @SuppressWarnings("SuspiciousToArrayCall")
     public <T> T[] toArray(final T[] array) {
-        return isSizeKnown() ? super.toArray(array) : toArray(array, false);
+        return isSizeKnown() ? super.toArray(array) : toArray(iterator(), array, false);
     }
 
     /**
-     * Implementation of the public {@code toArray()} methods.
+     * Implementation of the public {@code toArray()} methods without call to {@link #size()}.
      */
     @SuppressWarnings("unchecked")
-    private <T> T[] toArray(T[] array, boolean trimToSize) {
+    static <T> T[] toArray(final Iterator<?> it, T[] array, boolean trimToSize) {
         int i = 0;
-        for (final Iterator<E> it = iterator(); it.hasNext();) {
+        while (it.hasNext()) {
             if (i >= array.length) {
                 if (i >= Integer.MAX_VALUE >>> 1) {
                     throw new OutOfMemoryError("Required array size too large");
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/ListOfUnknownSizeTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/ListOfUnknownSizeTest.java
new file mode 100644
index 0000000..59a7cbc
--- /dev/null
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/ListOfUnknownSizeTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.util;
+
+import java.util.ListIterator;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link ListOfUnknownSize}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class ListOfUnknownSizeTest extends TestCase {
+    /**
+     * Tests {@link ListOfUnknownSize#listIterator()}.
+     */
+    @Test
+    public void testListIterator() {
+        final Integer[] data = new Integer[] {4, 7, 1, 3, 0, -4, -3};
+        final ListIterator<Integer> it = new ListOfUnknownSize<Integer>() {
+            @Override
+            protected boolean exists(int index) {
+                return index >= 0 && index < data.length;
+            }
+
+            @Override
+            public Integer get(int index) {
+                return data[index];
+            }
+        }.listIterator();
+
+        for (int i=0; i<data.length; i++) {
+            assertTrue(it.hasNext());
+            assertEquals(i, it.nextIndex());
+            assertEquals(data[i], it.next());
+        }
+        assertFalse(it.hasNext());
+    }
+}
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
index eef2619..3e69b65 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
@@ -69,6 +69,7 @@ import org.junit.BeforeClass;
 
     // Collections.
     org.apache.sis.internal.util.CheckedArrayListTest.class,
+    org.apache.sis.internal.util.ListOfUnknownSizeTest.class,
     org.apache.sis.internal.system.ReferenceQueueConsumerTest.class,
     org.apache.sis.util.collection.FrequencySortedSetTest.class,
     org.apache.sis.util.collection.IntegerListTest.class,
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
index ea256a1..e59a393 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -17,9 +17,7 @@
 package org.apache.sis.storage.geotiff;
 
 import java.util.Locale;
-import java.util.Iterator;
-import java.util.Collection;
-import java.util.NoSuchElementException;
+import java.util.List;
 import java.util.logging.LogRecord;
 import java.net.URI;
 import java.io.IOException;
@@ -45,7 +43,6 @@ import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
-import org.apache.sis.internal.referencing.LazySet;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.storage.MetadataBuilder;
@@ -53,6 +50,7 @@ import org.apache.sis.internal.storage.StoreUtilities;
 import org.apache.sis.internal.storage.URIDataStore;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.internal.util.ListOfUnknownSize;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.resources.Errors;
@@ -106,7 +104,7 @@ public class GeoTiffStore extends DataStore implements Aggregate {
      *
      * @see #components()
      */
-    private Collection<GridCoverageResource> components;
+    private List<GridCoverageResource> components;
 
     /**
      * Creates a new GeoTIFF store from the given file, URL or stream object.
@@ -253,48 +251,59 @@ public class GeoTiffStore extends DataStore implements Aggregate {
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public Collection<GridCoverageResource> components() throws DataStoreException {
+    public List<GridCoverageResource> components() throws DataStoreException {
         if (components == null) {
-            components = new LazySet<GridCoverageResource>(new Iterator<GridCoverageResource>() {
-                /** Index of the next image to fetch, or -1 if we fetched all of them. */
-                private int index;
+            components = new Components();
+        }
+        return components;
+    }
 
-                /** Value to be returned by {@link #next()}, or {@cod null} if not yet determined. */
-                private GridCoverageResource next;
+    /**
+     * The components returned by {@link #components}. Defined as a named class instead than an anonymous
+     * class for more readable stack trace. This is especially useful since {@link BackingStoreException}
+     * may happen in any method.
+     */
+    private final class Components extends ListOfUnknownSize<GridCoverageResource> {
+        /** The collection size, cached when first computed. */
+        private int size = -1;
 
-                /** Returns {@code true} if there is more resources. */
-                @Override public boolean hasNext() {
-                    if (next != null) {
-                        return true;
-                    }
-                    final int i = index;
-                    if (i >= 0) {
-                        index = -1;                     // Set now in case of failure.
-                        try {
-                            next = reader().getImageFileDirectory(i);
-                        } catch (IOException e) {
-                            throw new BackingStoreException(errorIO(e));
-                        } catch (DataStoreException e) {
-                            throw new BackingStoreException(e);
-                        }
-                        if (next != null) {
-                            index = i+1;
-                            return true;
-                        }
-                    }
-                    return false;
-                }
+        /** Returns the size or -1 if not yet known. */
+        @Override protected int sizeIfKnown() {
+            return size;
+        }
 
-                /** Returns the next element. */
-                @Override public GridCoverageResource next() {
-                    if (!hasNext()) throw new NoSuchElementException();
-                    final GridCoverageResource r = next;
-                    next = null;
-                    return r;
-                }
-            });
+        /** Returns the size, computing and caching it if needed. */
+        @Override public int size() {
+            if (size < 0) {
+                size = super.size();
+            }
+            return size;
+        }
+
+        /** Returns whether the given index is valid. */
+        @Override protected boolean exists(final int index) {
+            return (index >= 0) && getImageFileDirectory(index) != null;
+        }
+
+        /** Returns element at the given index or throw {@link IndexOutOfBoundsException}. */
+        @Override public GridCoverageResource get(final int index) {
+            if (index >= 0) {
+                GridCoverageResource image = getImageFileDirectory(index);
+                if (image != null) return image;
+            }
+            throw new IndexOutOfBoundsException(errors().getString(Errors.Keys.IndexOutOfBounds_1, index));
+        }
+
+        /** Returns element at the given index or returns {@code null} if the index is invalid. */
+        private GridCoverageResource getImageFileDirectory(final int index) {
+            try {
+                return reader().getImageFileDirectory(index);
+            } catch (IOException e) {
+                throw new BackingStoreException(errorIO(e));
+            } catch (DataStoreException e) {
+                throw new BackingStoreException(e);
+            }
         }
-        return components;      // Safe to return because unmodifiable.
     }
 
     /**