sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/02: Add more javadoc in GridCoverageBuilder and replace code copied from other classes by method calls. This required to modify the API of some invoked methods (package-private API only).
Date Tue, 24 Mar 2020 00:11:03 GMT
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 06a685dfe5f63e8b98b15c4d4a69a2ea646cfdb3
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Mar 24 00:48:11 2020 +0100

    Add more javadoc in GridCoverageBuilder and replace code copied from other classes by method calls.
    This required to modify the API of some invoked methods (package-private API only).
---
 .../org/apache/sis/coverage/grid/GridCoverage.java |   8 +-
 .../apache/sis/coverage/grid/GridCoverage2D.java   |  87 ++-
 .../sis/coverage/grid/GridCoverageBuilder.java     | 670 +++++++++++++--------
 .../org/apache/sis/coverage/grid/GridExtent.java   |   6 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |   2 +-
 .../coverage/j2d/BufferedGridCoverage.java         |  40 +-
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  17 +
 .../sis/internal/coverage/j2d/TiledImage.java      | 181 ++++++
 .../org/apache/sis/internal/feature/Resources.java |   5 +
 .../sis/internal/feature/Resources.properties      |   1 +
 .../sis/internal/feature/Resources_fr.properties   |   1 +
 .../sis/coverage/grid/GridCoverageBuilderTest.java |  17 +-
 .../org/apache/sis/geometry/CoordinateFormat.java  |   2 +-
 .../java/org/apache/sis/geometry/Envelopes.java    |   2 +-
 .../main/java/org/apache/sis/referencing/CRS.java  |   2 +-
 15 files changed, 744 insertions(+), 297 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index 4c84fa1..997845e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -97,15 +97,15 @@ public abstract class GridCoverage {
      * and the sample dimensions define the "range" (output) of that function.
      *
      * @param  domain  the grid extent, CRS and conversion from cell indices to CRS.
-     * @param  range   sample dimensions for each image band.
+     * @param  ranges  sample dimensions for each image band.
      * @throws NullPointerException if an argument is {@code null} or if the list contains a null element.
      * @throws IllegalArgumentException if the {@code range} list is empty.
      */
-    protected GridCoverage(final GridGeometry domain, final Collection<? extends SampleDimension> range) {
+    protected GridCoverage(final GridGeometry domain, final Collection<? extends SampleDimension> ranges) {
         ArgumentChecks.ensureNonNull ("domain", domain);
-        ArgumentChecks.ensureNonEmpty("range", range);
+        ArgumentChecks.ensureNonEmpty("ranges", ranges);
         gridGeometry = domain;
-        sampleDimensions = range.toArray(new SampleDimension[range.size()]);
+        sampleDimensions = ranges.toArray(new SampleDimension[ranges.size()]);
         ArgumentChecks.ensureNonEmpty("range", sampleDimensions);
         for (int i=0; i<sampleDimensions.length; i++) {
             ArgumentChecks.ensureNonNullElement("range", i, sampleDimensions[i]);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index c367672..b946a9b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -26,6 +26,7 @@ import java.text.NumberFormat;
 import java.text.FieldPosition;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.awt.Rectangle;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
@@ -185,8 +186,15 @@ public class GridCoverage2D extends GridCoverage {
      * @throws ArithmeticException if the distance between grid location and image location exceeds the {@code long} capacity.
      */
     public GridCoverage2D(GridGeometry domain, final Collection<? extends SampleDimension> range, RenderedImage data) {
-        super(domain = addExtentIfAbsent(domain, data = unwrapIfSameSize(data)), defaultIfAbsent(range, data));
-        this.data = data;           // Non-null verified by addExtentIfAbsent(…, data).
+        /*
+         * The complex nesting of method calls below is a workaround for RFE #4093999
+         * ("Relax constraint on placement of this()/super() call in constructors").
+         */
+        super(domain = addExtentIfAbsent(domain, data = unwrapIfSameSize(data)),
+                defaultIfAbsent(range, data, ImageUtilities.getNumBands(data)));
+
+        this.data = data;
+        ArgumentChecks.ensureNonNull("data", data);
         /*
          * Find indices of the two dimensions of the slice. Those dimensions are usually 0 for x and 1 for y,
          * but not necessarily. A two dimensional CRS will be extracted for those dimensions later if needed.
@@ -237,14 +245,33 @@ public class GridCoverage2D extends GridCoverage {
      * with an extent computed from the given image. The new grid will start at the same
      * location than the image and will have the same size.
      *
-     * <p>This static method is a workaround for RFE #4093999
-     * ("Relax constraint on placement of this()/super() call in constructors").</p>
+     * @param  domain  the domain to complete. May be {@code null}.
+     * @param  data    user-supplied image, or {@code null} if missing.
+     * @return the potentially completed domain (may be {@code null}).
      */
-    @Workaround(library="JDK", version="1.8")
-    private static GridGeometry addExtentIfAbsent(GridGeometry domain, final RenderedImage data) {
-        ArgumentChecks.ensureNonNull("data", data);
+    static GridGeometry addExtentIfAbsent(GridGeometry domain, final RenderedImage data) {
+        if (data != null) {
+            domain = addExtentIfAbsent(domain, ImageUtilities.getBounds(data));
+        }
+        return domain;
+    }
+
+    /**
+     * If the given domain does not have a {@link GridExtent}, creates a new grid geometry
+     * with an extent computed from the given image bounds. The new grid will start at the
+     * same location than the image and will have the same size.
+     *
+     * <p>This method does nothing if the given domain already has an extent;
+     * it does not verify that the extent is consistent with image size.
+     * This verification should be done by the caller.</p>
+     *
+     * @param  domain  the domain to complete. May be {@code null}.
+     * @param  bounds  image or raster bounds (can not be {@code null}).
+     * @return the potentially completed domain (may be {@code null}).
+     */
+    static GridGeometry addExtentIfAbsent(GridGeometry domain, final Rectangle bounds) {
         if (domain == null) {
-            GridExtent extent = new GridExtent(data.getMinX(), data.getMinY(), data.getWidth(), data.getHeight());
+            GridExtent extent = new GridExtent(bounds.x, bounds.y, bounds.width, bounds.height);
             domain = new GridGeometry(extent, PixelInCell.CELL_CENTER, null, null);
         } else if (!domain.isDefined(GridGeometry.EXTENT)) {
             final int dimension = domain.getDimension();
@@ -253,7 +280,7 @@ public class GridCoverage2D extends GridCoverage {
                 if (domain.isDefined(GridGeometry.CRS)) {
                     crs = domain.getCoordinateReferenceSystem();
                 }
-                final GridExtent extent = createExtent(dimension, data, crs);
+                final GridExtent extent = createExtent(dimension, bounds, crs);
                 if (domain.isDefined(GridGeometry.GRID_TO_CRS)) try {
                     domain = new GridGeometry(domain, extent, null);
                 } catch (TransformException e) {
@@ -287,7 +314,7 @@ public class GridCoverage2D extends GridCoverage {
      * @see GridGeometry#GridGeometry(GridExtent, Envelope)
      */
     public GridCoverage2D(final Envelope domain, final Collection<? extends SampleDimension> range, final RenderedImage data) {
-        super(createGridGeometry(data, domain), defaultIfAbsent(range, data));
+        super(createGridGeometry(data, domain), defaultIfAbsent(range, data, ImageUtilities.getNumBands(data)));
         this.data = data;   // Non-null verified by createGridGeometry(…, data).
         xDimension   = 0;
         yDimension   = 1;
@@ -315,27 +342,27 @@ public class GridCoverage2D extends GridCoverage {
             }
             crs = envelope.getCoordinateReferenceSystem();
         }
-        return new GridGeometry(createExtent(dimension, data, crs), envelope);
+        return new GridGeometry(createExtent(dimension, ImageUtilities.getBounds(data), crs), envelope);
     }
 
     /**
-     * Creates a grid extent with the low and high coordinates of the given image.
+     * Creates a grid extent with the low and high coordinates of the given image bounds.
      * The coordinate reference system is used for extracting grid axis names, in particular
      * the {@link DimensionNameType#VERTICAL} and {@link DimensionNameType#TIME} dimensions.
      * The {@link DimensionNameType#COLUMN} and {@link DimensionNameType#ROW} dimensions can
      * not be inferred from CRS analysis; they are added from knowledge that we have an image.
      *
      * @param  dimension  number of dimensions.
-     * @param  data       the image for which to create a grid extent.
+     * @param  bounds     bounds of the image for which to create a grid extent.
      * @param  crs        coordinate reference system, or {@code null} if none.
      */
-    private static GridExtent createExtent(final int dimension, final RenderedImage data, final CoordinateReferenceSystem crs) {
+    private static GridExtent createExtent(final int dimension, final Rectangle bounds, final CoordinateReferenceSystem crs) {
         final long[] low  = new long[dimension];
         final long[] high = new long[dimension];
-        low [0] = data.getMinX();
-        low [1] = data.getMinY();
-        high[0] = data.getWidth()  + low[0] - 1;        // Inclusive.
-        high[1] = data.getHeight() + low[1] - 1;
+        low [0] = bounds.x;
+        low [1] = bounds.y;
+        high[0] = bounds.width  + low[0] - 1;        // Inclusive.
+        high[1] = bounds.height + low[1] - 1;
         DimensionNameType[] axisTypes = GridExtent.typeFromAxes(crs, dimension);
         if (axisTypes == null) {
             axisTypes = new DimensionNameType[dimension];
@@ -346,20 +373,26 @@ public class GridCoverage2D extends GridCoverage {
     }
 
     /**
-     * If the sample dimensions are null, creates default sample dimensions
-     * with "gray", "red, green, blue" or "cyan, magenta, yellow" names.
+     * If the sample dimensions are null, creates default sample dimensions with default names.
+     * The default names are "gray", "red, green, blue" or "cyan, magenta, yellow" if the color
+     * model is identified as such, or numbers if the color model is not recognized.
+     *
+     * @param  range     the list of sample dimensions, potentially null.
+     * @param  data      the image for which to build sample dimensions, or {@code null}.
+     * @param  numBands  the number of bands in the given image, or 0 if none.
+     * @return the given list of sample dimensions if it was non-null, or a default list otherwise.
      */
-    private static Collection<? extends SampleDimension> defaultIfAbsent(
-            Collection<? extends SampleDimension> range, final RenderedImage data)
+    static Collection<? extends SampleDimension> defaultIfAbsent(Collection<? extends SampleDimension> range,
+                                                                 final RenderedImage data, final int numBands)
     {
         if (range == null) {
-            final short[] names = ImageUtilities.bandNames(data);
-            final SampleDimension[] sd = new SampleDimension[names.length];
+            final short[] names = (data != null) ? ImageUtilities.bandNames(data) : ArraysExt.EMPTY_SHORT;
+            final SampleDimension[] sd = new SampleDimension[numBands];
             final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
-            for (int i=0; i<names.length; i++) {
+            for (int i=0; i<numBands; i++) {
                 final InternationalString name;
-                final short k = names[0];
-                if (k != 0) {
+                final short k;
+                if (i < names.length && (k = names[i]) != 0) {
                     name = Vocabulary.formatInternational(k);
                 } else {
                     name = Vocabulary.formatInternational(Vocabulary.Keys.Band_1, i+1);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index 182a71c..c7a0d76 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -16,83 +16,323 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.awt.Point;
+import java.awt.Dimension;
+import java.awt.Rectangle;
 import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.internal.coverage.j2d.BufferedGridCoverage;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
-import org.apache.sis.internal.util.DoubleDouble;
+import org.apache.sis.internal.coverage.j2d.TiledImage;
+import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.ArraysExt;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.spatial.DimensionNameType;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.datum.PixelInCell;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
- * Helper class for the creation of {@link GridCoverage} instances. This builder can creates the
- * parameters to be given to {@linkplain GridCoverage2D} and {@linkplain BufferedGridCoverage}
- * from simpler parameters given to this builder.
+ * Helper class for the creation of {@link GridCoverage} instances.
+ * A grid coverage is a function described by three parts:
+ *
+ * <ul>
+ *   <li>A <cite>domain</cite>, which describes the input values (e.g. geographic coordinates).</li>
+ *   <li>One or more <cite>ranges</cite>, which describe the output values that the coverage can produce.</li>
+ *   <li>The actual values, distributed on a regular grid.</li>
+ * </ul>
+ *
+ * Each of those parts can be set by a {@code setDomain(…)}, {@code setRanges(…)} or {@code setValues(…)} method.
+ * Those methods are overloaded with many variants accepting different kind of arguments. For example values can
+ * be specified as a {@link RenderedImage}, a {@link Raster} or some other types.
+ *
+ * <div class="note"><b>Example:</b>
+ * the easiest way to create a {@link GridCoverage} from a matrix of values is to set the values in a
+ * {@link WritableRaster} and to specify the domain as an {@link Envelope}:
+ *
+ * {@preformat java
+ *     WritableRaster data = Raster.createBandedRaster​(DataBuffer.TYPE_USHORT, width, height, numBands, null);
+ *     for (int y=0; y<height; y++) {
+ *         for (int x=0; x<width; x++) {
+ *             int value = ...;                     // Compute a value here.
+ *             data.setSample(x, y, 0, value);      // Set value in the first band.
+ *         }
+ *     }
+ *     GridCoverageBuilder builder = new GridCoverageBuilder();
+ *     builder.setValues(data).flixAxis(1);
+ *
+ *     Envelope domain = ...;                       // Specify here the "real world" coordinates.
+ *     GridCoverage coverage = builder.setDomain(domain).build();
+ * }
+ * </div>
+ *
+ * Current implementation creates only two-dimensional coverages.
+ * A future version may extend this builder API for creating <var>n</var>-dimensional coverages.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @see GridCoverage2D
+ * @see SampleDimension.Builder
  *
- * @author Johann Sorel (Geomatys)
+ * @since 1.1
+ * @module
  */
 public class GridCoverageBuilder {
+    /**
+     * The domain (input) of the coverage function, or {@code null} if unspecified.
+     * If {@code null}, an identify "grid to CRS" transform will be assumed.
+     *
+     * @see #setDomain(GridGeometry)
+     * @see #setDomain(Envelope)
+     */
+    private GridGeometry domain;
 
+    /**
+     * The range (output) of the coverage function, or {@code null} if unspecified.
+     * If non-null, then the size of this list must be equal to the number of bands.
+     *
+     * @see #setRanges(Collection)
+     * @see #setRanges(SampleDimension...)
+     * @see #addRange(SampleDimension)
+     */
     private List<SampleDimension> ranges;
-    private WritableRaster raster;
+
+    /**
+     * The band to be made visible (usually 0). All other bands, if any will be hidden.
+     * This is used only for data without color model as {@link #raster} and {@link #buffer}.
+     *
+     * @todo There is not yet a setter method for this property.
+     */
+    private int visibleBand;
+
+    /**
+     * The raster containing the coverage values.
+     * Exactly one of {@code image}, {@link #raster} and {@link #buffer} shall be non-null.
+     *
+     * @see #setValues(RenderedImage)
+     */
     private RenderedImage image;
+
+    /**
+     * The raster containing the coverage values.
+     * May be a {@link WritableRaster}, in which case a {@link BufferedImage} may be created.
+     * Exactly one of {@link #image}, {@code raster} and {@link #buffer} shall be non-null.
+     *
+     * @see #setValues(Raster)
+     */
+    private Raster raster;
+
+    /**
+     * The data buffer containing the coverage values.
+     * Exactly one of {@link #image}, {@link #raster} and {@code #buffer} shall be non-null.
+     *
+     * @see #setValues(DataBuffer, Dimension)
+     */
     private DataBuffer buffer;
-    private int bufferWidth = -1;
-    private int bufferHeight = -1;
-    private int bufferNbSample = -1;
-    private GridGeometry grid;
-    private final Set<Integer> flippedAxis = new HashSet<>();
 
     /**
-     * Sets coverage data rendered image.
+     * The image size, or {@code null} if unspecified. It needs to be specified only
+     * if values were specified as a buffer without information about the grid size.
+     *
+     * @see #setValues(DataBuffer, Dimension)
+     */
+    private Dimension size;
+
+    /**
+     * Set of grid axes to reverse, as a bit mask. For any dimension <var>i</var>, the bit
+     * at {@code 1L << i} is set to 1 if the grid axis at that dimension should be flipped.
+     *
+     * @see #flipAxis(int)
+     */
+    private long flippedAxes;
+
+    /**
+     * Creates an initially empty builder.
+     */
+    public GridCoverageBuilder() {
+    }
+
+    /**
+     * Sets the domain envelope (including its CRS) and/or the transform from grid indices to domain coordinates.
+     * The given {@code GridGeometry} does not need to contain a {@link GridExtent} because that extent will be
+     * computed automatically if needed. However if an extent is present, then it must be consistent with the
+     * size of data given to {@code setValues(…)} method (will be verified at {@link #build()} time).
+     *
+     * @param  grid  the new grid geometry, or {@code null} for removing previous domain setting.
+     * @return {@code this} for method invocation chaining.
+     */
+    public GridCoverageBuilder setDomain(final GridGeometry grid) {
+        domain = grid;
+        return this;
+    }
+
+    /**
+     * Sets the domain envelope (including its CRS).
+     * If the given envelope contains a CRS, then that CRS will be the coverage CRS.
+     * A transform from grid indices to domain coordinates will be created automatically.
+     * That transform will map grid dimensions to envelope dimensions in the same order
+     * (i.e. the matrix representation of the affine transform will be diagonal,
+     * ignoring the translation column).
      *
-     * @param image The rendered image to be wrapped by {@code GridCoverage2D}, not {@code null}.
+     * <h4>Axis directions</h4>
+     * By default grid indices increase in the same direction than domain coordinates.
+     * When applied to images with pixels located by (<var>column</var>, <var>row</var>) indices,
+     * it means that by default row indices in the image are increasing toward up if the <var>y</var>
+     * coordinates in the coverage domain (e.g. latitude values) are also increasing toward up.
+     * It often results in images flipped vertically, because popular image formats such as PNG
+     * use row indices increasing in the opposite direction (toward down).
+     * This effect can be compensated by invoking <code>{@linkplain #flipAxis(int) flipAxis}(1)</code>.
+     *
+     * <div class="note"><b>Design note:</b>
+     * {@code GridCoverageBuilder} does not flip the <var>y</var> axis by default because not all
+     * file formats have row indices increasing toward down. A counter-example is the netCDF format.
+     * Even if we consider that the majority of images have <var>y</var> axis flipped, things become
+     * less obvious when considering data in more than two dimensions. Having the same default policy
+     * (no flipping) for all dimensions make problem analysis easier.</div>
+     *
+     * {@code GridCoverageBuilder} provides method only for flipping axes.
+     * If more sophisticated operations is desired (for example a rotation),
+     * then {@link #setDomain(GridGeometry)} should be used instead than this method.
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation creates a new {@link GridGeometry} from the given envelope
+     * then invokes {@link #setDomain(GridGeometry)}. Subclasses can override that later method
+     * as a single overriding point for all domain settings.
+     *
+     * @param  domain  envelope of the coverage domain together with its CRS,
+     *                 or {@code null} for removing previous domain setting.
+     * @return {@code this} for method invocation chaining.
+     *
+     * @see #flipAxis(int)
+     * @see GridGeometry#GridGeometry(GridExtent, Envelope)
+     */
+    public GridCoverageBuilder setDomain(final Envelope domain) {
+        return setDomain(domain == null ? null : new GridGeometry(null, domain));
+    }
+
+    /**
+     * Sets the sample dimensions for all bands.
+     * The list size must be equal to the number of bands in the data specified to
+     * {@code setValues(…)} method (it will be verified at {@link #build()} time).
+     *
+     * @param  bands  the new sample dimensions, or {@code null} for removing previous range setting.
+     * @return {@code this} for method invocation chaining.
+     * @throws IllegalArgumentException if the given list is empty.
+     *
+     * @see SampleDimension.Builder
      */
-    public GridCoverageBuilder setValues(RenderedImage image) {
-        ArgumentChecks.ensureNonNull("image", image);
-        this.image = image;
-        this.raster = null;
-        this.buffer = null;
-        this.bufferWidth = -1;
-        this.bufferHeight = -1;
-        this.bufferNbSample = -1;
+    public GridCoverageBuilder setRanges(final Collection<? extends SampleDimension> bands) {
+        if (bands == null) {
+            ranges = null;
+        } else {
+            ArgumentChecks.ensureNonEmpty("bands", bands);
+            if (ranges instanceof ArrayList<?>) {
+                ranges.clear();
+                ranges.addAll(bands);
+            } else {
+                ranges = new ArrayList<>(bands);
+            }
+        }
         return this;
     }
 
     /**
-     * Sets coverage data raster.
+     * Sets the sample dimensions for all bands.
+     * The array length must be equal to the number of bands in the data specified to
+     * {@code setValues(…)} method (it will be verified at {@link #build()} time).
+     *
+     * @param  bands  the new sample dimensions, or {@code null} for removing previous range setting.
+     * @return {@code this} for method invocation chaining.
+     * @throws IllegalArgumentException if the given array is empty.
      *
-     * @param raster The raster to be wrapped by {@code GridCoverage2D}, not {@code null}.
+     * @see SampleDimension.Builder
      */
-    public GridCoverageBuilder setValues(WritableRaster raster) {
-        ArgumentChecks.ensureNonNull("raster", raster);
-        this.image = null;
-        this.raster = raster;
-        this.buffer = null;
-        this.bufferWidth = -1;
-        this.bufferHeight = -1;
-        this.bufferNbSample = -1;
+    public GridCoverageBuilder setRanges(final SampleDimension... bands) {
+        if (bands == null) {
+            ranges = null;
+        } else {
+            ArgumentChecks.ensureNonEmpty("bands", bands);
+            ranges = Arrays.asList(bands);
+        }
+        return this;
+    }
+
+    /**
+     * Adds a sample dimension for one band. This method can be invoked repeatedly until the number of
+     * sample dimensions is equal to the number of bands in the data specified to {@code setValues(…)}.
+     *
+     * @param  band  the sample dimension to add.
+     * @return {@code this} for method invocation chaining.
+     *
+     * @see SampleDimension.Builder
+     */
+    public GridCoverageBuilder addRange(final SampleDimension band) {
+        ArgumentChecks.ensureNonNull("band", band);
+        if (!(ranges instanceof ArrayList<?>)) {
+            ranges = (ranges != null) ? new ArrayList<>(ranges) : new ArrayList<>();
+        }
+        ranges.add(band);
+        return this;
+    }
+
+    /**
+     * Sets a two-dimensional slice of sample values as a rendered image.
+     * If {@linkplain #setRanges(SampleDimension...) sample dimensions are specified},
+     * then the {@linkplain java.awt.image.SampleModel#getNumBands() number of bands}
+     * must be equal to the number of sample dimensions.
+     *
+     * <p><b>Note:</b> row indices in an image are usually increasing down, while geographic coordinates
+     * are usually increasing up. Consequently the <code>{@linkplain #flipAxis(int) flipAxis}(1)</code>
+     * method may need to be invoked after this method.</p>
+     *
+     * @param  data  the rendered image to be wrapped in a {@code GridCoverage}. Can not be {@code null}.
+     * @return {@code this} for method invocation chaining.
+     *
+     * @see BufferedImage
+     */
+    public GridCoverageBuilder setValues(final RenderedImage data) {
+        ArgumentChecks.ensureNonNull("data", data);
+        image  = data;
+        raster = null;
+        buffer = null;
+        size   = null;
+        return this;
+    }
+
+    /**
+     * Sets a two-dimensional slice of sample values as a raster.
+     * If {@linkplain #setRanges(SampleDimension...) sample dimensions are specified},
+     * then the {@linkplain Raster#getNumBands() number of bands} must be equal to the
+     * number of sample dimensions.
+     *
+     * <p><b>Note:</b> row indices in a raster are usually increasing down, while geographic coordinates
+     * are usually increasing up. Consequently the <code>{@linkplain #flipAxis(int) flipAxis}(1)</code>
+     * method may need to be invoked after this method.</p>
+     *
+     * @param  data  the raster to be wrapped in a {@code GridCoverage}. Can not be {@code null}.
+     * @return {@code this} for method invocation chaining.
+     *
+     * @see Raster#createBandedRaster(int, int, int, int, Point)
+     */
+    public GridCoverageBuilder setValues(final Raster data) {
+        ArgumentChecks.ensureNonNull("data", data);
+        raster = data;
+        image  = null;
+        buffer = null;
+        size   = null;
         return this;
     }
 
@@ -163,233 +403,183 @@ public class GridCoverageBuilder {
 //    }
 
     /**
-     * Creates a coverage from the given buffer.
-     * This method uses the given buffer unmodified to create the coverage.
+     * Sets a two-dimensional slice of sample values as a Java2D data buffer.
+     * The {@linkplain DataBuffer#getNumBanks() number of banks} will be the number of bands in the image.
+     * If {@linkplain #setRanges(SampleDimension...) sample dimensions are specified}, then the number of
+     * bands must be equal to the number of sample dimensions.
      *
-     * @param data the coverage datas, not {@code null}.
+     * @param  data  the data buffer to be wrapped in a {@code GridCoverage}. Can not be {@code null}.
+     * @param  size  the image size in pixels, or {@code null} if unspecified. If null, then the image
+     *               size will be taken from the {@linkplain GridGeometry#getExtent() grid extent}.
+     * @return {@code this} for method invocation chaining.
+     * @throws IllegalArgumentException if {@code width} or {@code height} is negative or equals to zero.
      */
-    public GridCoverageBuilder setValues(DataBuffer data) {
+    public GridCoverageBuilder setValues(final DataBuffer data, Dimension size) {
         ArgumentChecks.ensureNonNull("data", data);
-        this.image = null;
-        this.raster = null;
-        this.buffer = data;
-        this.bufferWidth = -1;
-        this.bufferHeight = -1;
-        this.bufferNbSample = -1;
+        if (size != null) {
+            size = new Dimension(size);
+            ArgumentChecks.ensureStrictlyPositive("width",  size.width);
+            ArgumentChecks.ensureStrictlyPositive("height", size.height);
+        }
+        this.size = size;
+        buffer = data;
+        image  = null;
+        raster = null;
         return this;
     }
 
-    private void setValues(DataBuffer data, int width, int height) {
-        this.image = null;
-        this.raster = null;
-        this.buffer = data;
-        this.bufferWidth = width;
-        this.bufferHeight = height;
-        this.bufferNbSample = 1;
-    }
-
     /**
-     * Sets the grid geometry to the given envelope.
-     * This method creates a new {@link GridGeometry}
-     * then invokes {@link #setDomain(GridGeometry)}.
+     * Reverses axis direction in the specified grid dimension.
+     * For example if grid indices are (<var>column</var>, <var>row</var>),
+     * then {@code flipAxis(1)} will reverse the direction of rows axis.
+     * Invoking this method a second time for the same dimension will cancel the flipping.
      *
-     * @param envelope The new grid geometry envelope, or {@code null}.
-     */
-    public GridCoverageBuilder setDomain(Envelope envelope) {
-        return setDomain(envelope == null ? null : new GridGeometry(null, envelope));
-    }
-
-    /**
-     * Sets the grid geometry to the given value.
+     * <p>When building coverage with a {@linkplain #setDomain(Envelope) domain specified by an envelope}
+     * (i.e. with no explicit <cite>grid to CRS</cite> transform), the default {@code GridCoverageBuilder}
+     * behavior is to create a {@link GridGeometry} with grid indices increasing in the same direction than
+     * domain coordinates. This method allows to reverse direction for an axis.
+     * The most typical usage is to reverse the direction of the <var>y</var> axis in images.</p>
      *
-     * @param grid The new grid geometry, or {@code null}.
-     */
-    public GridCoverageBuilder setDomain(GridGeometry grid) {
-        this.grid = grid;
-        return this;
-    }
-
-    /**
-     * Sets all sample dimensions.
+     * @param  dimension  index of the dimension in the grid to reverse direction.
+     * @return {@code this} for method invocation chaining.
      *
-     * @param range The new sample dimensions, or {@code null}.
+     * @see #setDomain(Envelope)
      */
-    public GridCoverageBuilder setRanges(final SampleDimension... range) {
-        this.ranges = (range == null) ? null : new ArrayList<>(Arrays.asList(range));
+    public GridCoverageBuilder flipAxis(final int dimension) {
+        ArgumentChecks.ensurePositive("dimension", dimension);
+        if (dimension >= Long.SIZE) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension + 1));
+        }
+        flippedAxes ^= (1L << dimension);
         return this;
     }
 
     /**
-     * Sets all sample dimensions.
+     * Creates the grid coverage from the domain, ranges and values given to setter methods.
+     * The returned coverage is often a {@link GridCoverage2D} instance, but not necessarily.
      *
-     * @param range The new sample dimensions, or {@code null}.
+     * @return grid coverage created from specified domain, ranges and sample values.
+     * @throws IllegalStateException if some properties are inconsistent, for example
+     *         {@linkplain GridGeometry#getExtent() grid extent} not matching image size or
+     *         {@linkplain #setRanges(SampleDimension...) number of sample dimensions} not matching
+     *         the number of bands. This exception often wraps an {@link IllegalGridGeometryException},
+     *         {@link IllegalArgumentException} or {@link org.apache.sis.util.NullArgumentException}.
      */
-    public GridCoverageBuilder setRanges(Collection<? extends SampleDimension> range) {
-        this.ranges = (range == null) ? null : new ArrayList<>(range);
-        return this;
+    public GridCoverage build() throws IllegalStateException {
+        GridGeometry grid = domain;                                 // May be replaced by an instance with extent.
+        Collection<? extends SampleDimension> bands = ranges;       // May be replaced by a non-null value.
+        /*
+         * If not already done, create the image from the raster. We try to create the most standard objects
+         * when possible: a BufferedImage (from Java2D), then later a GridCoverage2D (from SIS public API).
+         * An exception to this rule is the DataBuffer case: we use a dedicated BufferedGridCoverage class
+         * instead.
+         */
+        try {
+            if (image == null) {
+                if (raster == null) {
+                    if (buffer == null) {
+                        throw new IllegalStateException(missingProperty("values"));
+                    }
+                    if (size != null) {
+                        grid = GridCoverage2D.addExtentIfAbsent(grid, new Rectangle(size));
+                        verifyGridExtent(grid.getExtent(), size.width, size.height);
+                    } else if (grid == null) {
+                        throw new IncompleteGridGeometryException(missingProperty("size"));
+                    }
+                    bands = GridCoverage2D.defaultIfAbsent(bands, null, buffer.getNumBanks());
+                    return new BufferedGridCoverage(domainWithAxisFlips(grid), bands, buffer);
+                }
+                /*
+                 * If the band list is null, create a default list of bands because we need
+                 * them for creating the color model. Note that we shall not do that when a
+                 * RenderedImage has been specified because GridCoverage2D constructor will
+                 * will infer better names.
+                 */
+                bands = GridCoverage2D.defaultIfAbsent(bands, null, raster.getNumBands());
+                final int dataType = raster.getSampleModel().getDataType();
+                final ColorModel colors = ColorModelFactory.createColorModel(
+                        bands.toArray(new SampleDimension[bands.size()]),
+                        visibleBand, dataType, ColorModelFactory.GRAYSCALE);
+                /*
+                 * Create an image from the raster. We favor BufferedImage instance when possible,
+                 * and fallback on TiledImage only if the BufferedImage can not be created.
+                 */
+                if (raster instanceof WritableRaster && raster.getMinX() == 0 && raster.getMinY() == 0) {
+                    image = new BufferedImage(colors, (WritableRaster) raster, false, null);
+                } else {
+                    image = new TiledImage(colors, raster.getWidth(), raster.getHeight(), 0, 0, raster);
+                }
+            }
+            /*
+             * At this point `image` shall be non-null but `bands` may still be null (it is okay).
+             */
+            return new GridCoverage2D(domainWithAxisFlips(grid), bands, image);
+        } catch (TransformException | NullPointerException | IllegalArgumentException | ArithmeticException e) {
+            throw new IllegalStateException(Resources.format(Resources.Keys.CanNotBuildGridCoverage), e);
+        }
     }
 
     /**
-     * When building coverage with a grid geometry without a grid to crs transform
-     * the grid to crs is computed automaticaly.
-     * The default behavior creates a grid geometry with increasing values on all
-     * axis. This method allows to reverse direction on an axis.
+     * Returns the {@linkplain #domain} with axis flips applied. If there is no axis to flip,
+     * {@link #domain} is returned unchanged (without completion for missing extent; we leave
+     * that to {@link GridCoverage2D} constructor).
      *
-     * @param dimension
+     * @see GridCoverage2D#addExtentIfAbsent(GridGeometry, Rectangle)
      */
-    public GridCoverageBuilder flipAxis(int dimension) {
-        ArgumentChecks.ensurePositive("idx", dimension);
-        flippedAxis.add(dimension);
-        return this;
+    private GridGeometry domainWithAxisFlips(GridGeometry grid) throws TransformException {
+        long f = flippedAxes;
+        if (f != 0) {
+            grid = GridCoverage2D.addExtentIfAbsent(grid, image);
+            if (grid != null && grid.isDefined(GridGeometry.EXTENT)) {
+                final GridExtent extent = grid.getExtent();
+                final int srcDim = extent.getDimension();
+                final MatrixSIS flip = Matrices.createDiagonal(grid.getTargetDimension() + 1, srcDim + 1);
+                do {
+                    final int j = Long.numberOfTrailingZeros(f);
+                    flip.setElement(j, j, -1);
+                    flip.setElement(j, srcDim, extent.getSize(j, false));
+                    f &= ~(1L << j);
+                } while (f != 0);
+                grid = new GridGeometry(grid, extent, MathTransforms.linear(flip));
+            }
+        }
+        return grid;
     }
 
     /**
-     * Creates the grid coverage.
-     * Current implementation may create a {@link BufferedGridCoverage} or {@link GridCoverage2D},
-     * but future implementations may instantiate different other coverage types.
+     * Verifies that the grid extent has the expected size. This method does not verify grid location
+     * (low coordinates) because it is okay to have it anywhere. The {@code expectedSize} array can be
+     * shorter than the number of dimensions (i.e. it may be a slice in a data cube); this method uses
+     * {@link GridExtent#getSubspaceDimensions(int)} for determining which dimensions to check.
+     *
+     * <p>This verification can be useful because {@link DataBuffer} does not contain any information
+     * about image size, so {@link BufferedGridCoverage#render(GridExtent)} will rely on the size
+     * provided by the grid extent. If those information do not reflect accurately the image size,
+     * the image will not be rendered properly.</p>
      *
-     * @return created coverage
-     * @throws IllegalGridGeometryException if the {@code domain} does not met the above-documented conditions.
-     * @throws IllegalArgumentException if the image number of bands is not the same than the number of sample dimensions.
+     * @param  extent        the extent to verify.
+     * @param  expectedSize  the expected image size.
+     * @throws IllegalGridGeometryException if the extent does not have the expected size.
      */
-    public GridCoverage build() {
-
-        GridGeometry grid = this.grid;
-        List<SampleDimension> ranges = this.ranges;
-        RenderedImage image = this.image;
-
-        //create an image from raster
-        if (raster != null) {
-            final int dataType = raster.getSampleModel().getDataType();
-            final int numBands = raster.getSampleModel().getNumBands();
-
-            if (ranges == null) {
-                ranges = new ArrayList<>(numBands);
-                for (int i = 0; i < numBands; i++) {
-                    ranges.add(new SampleDimension.Builder().setName(i).build());
-                }
+    private static void verifyGridExtent(final GridExtent extent, final int... expectedSize) {
+        final int[] imageAxes = extent.getSubspaceDimensions(expectedSize.length);
+        for (int i=0; i<expectedSize.length; i++) {
+            final int imageSize = expectedSize[i];
+            final long gridSize = extent.getSize(imageAxes[i]);
+            if (imageSize != gridSize) {
+                throw new IllegalGridGeometryException(Resources.format(
+                        Resources.Keys.MismatchedImageSize_3, i, imageSize, gridSize));
             }
-
-            final ColorModel colors = ColorModelFactory.createColorModel(ranges.toArray(new SampleDimension[0]), 0, dataType, ColorModelFactory.GRAYSCALE);
-            image = new BufferedImage(colors, raster, false, null);
-        }
-
-        if (image != null) {
-            grid = addExtentIfAbsent(grid, image.getWidth(), image.getHeight(), flippedAxis);
-            //use provided ranges, even if null, GridCoverage2D makes a better work at building them
-            return new GridCoverage2D(grid, this.ranges, image);
-        } else if (buffer != null) {
-
-            //verify and enrich grid geometry
-            if (bufferWidth != -1) {
-                if (grid.isDefined(GridGeometry.EXTENT)) {
-                    GridExtent extent = grid.getExtent();
-                    if (extent.getDimension() != 2) {
-                        throw new IllegalGridGeometryException("Grid dimension differ from buffer size, expected 2 found " + extent.getDimension());
-                    } else if (extent.getSize(0) != bufferWidth) {
-                        throw new IllegalGridGeometryException("Grid width differ from buffer width, expected " + bufferWidth + " found " + extent.getSize(0));
-                    } else if (extent.getSize(1) != bufferHeight) {
-                        throw new IllegalGridGeometryException("Grid height differ from buffer height, expected " + bufferHeight + " found " + extent.getSize(1));
-                    }
-                } else {
-                    grid = addExtentIfAbsent(grid, bufferWidth, bufferHeight, flippedAxis);
-                }
-            }
-            //verify sample dimensions
-            if (bufferNbSample != -1) {
-                if (ranges != null && ranges.size() != bufferNbSample) {
-                    throw new IllegalArgumentException("Sample dimension list differ from matrix, expected " + bufferNbSample + " found " + ranges.size());
-                }
-                if (ranges == null) {
-                    //create default dimensions
-                    ranges = new ArrayList<>(bufferNbSample);
-                    for (int i = 0; i < bufferNbSample; i++) {
-                        ranges.add(new SampleDimension.Builder().setName(i).build());
-                    }
-                }
-            }
-
-            return new BufferedGridCoverage(grid, ranges, buffer);
-        } else {
-            throw new IllegalArgumentException("Image, buffer or matrix must be set before building coverage.");
         }
     }
 
     /**
-     * If the given domain does not have a {@link GridExtent}, creates a new grid geometry
-     * with an extent of given size.
+     * Returns an error message for the exception to thrown when a mandatory property is missing.
+     *
+     * @param  name  name of the missing property.
+     * @return message for the exception to throw.
      */
-    private static GridGeometry addExtentIfAbsent(GridGeometry domain, int width, int height, Set<Integer> flippedAxis) {
-        if (domain == null) {
-            GridExtent extent = new GridExtent(width, height);
-            domain = new GridGeometry(extent, PixelInCell.CELL_CENTER, null, null);
-        } else if (!domain.isDefined(GridGeometry.EXTENT)) {
-            final int dimension = domain.getDimension();
-            if (dimension >= 2) {
-                CoordinateReferenceSystem crs = null;
-                if (domain.isDefined(GridGeometry.CRS)) {
-                    crs = domain.getCoordinateReferenceSystem();
-                }
-                final long[] low  = new long[dimension];
-                final long[] high = new long[dimension];
-                high[0] = width - 1;        // Inclusive.
-                high[1] = height - 1;
-                DimensionNameType[] axisTypes = GridExtent.typeFromAxes(crs, dimension);
-                if (axisTypes == null) {
-                    axisTypes = new DimensionNameType[dimension];
-                }
-                if (!ArraysExt.contains(axisTypes, DimensionNameType.COLUMN)) axisTypes[0] = DimensionNameType.COLUMN;
-                if (!ArraysExt.contains(axisTypes, DimensionNameType.ROW))    axisTypes[1] = DimensionNameType.ROW;
-                final GridExtent extent = new GridExtent(axisTypes, low, high, true);
-                if (domain.isDefined(GridGeometry.GRID_TO_CRS)) {
-                    try {
-                        domain = new GridGeometry(domain, extent, null);
-                    } catch (TransformException e) {
-                        throw new IllegalGridGeometryException(e);                  // Should never happen.
-                    }
-                } else if (flippedAxis.isEmpty()) {
-                    domain = new GridGeometry(extent, domain.envelope);
-                } else {
-                    // create transform with flipped axis
-                    boolean nilEnvelope = true;
-                    final ImmutableEnvelope env = ImmutableEnvelope.castOrCopy(domain.envelope);
-                    if (env == null || ((nilEnvelope = env.isAllNaN()) && env.getCoordinateReferenceSystem() == null)) {
-                        //do nothing
-                    } else if (!nilEnvelope) {
-                        /*
-                         * If we have both the extent and an envelope with at least one non-NaN coordinates,
-                         * create the `cornerToCRS` transform. The `gridToCRS` calculation uses the knowledge
-                         * that all scale factors are on diagonal with no sign reversal, which allows simpler
-                         * calculation than full matrix multiplication. Use double-double arithmetic everywhere.
-                         */
-                        final MatrixSIS affine = extent.cornerToCRS(env);
-                        final MathTransform cornerToCRS = MathTransforms.linear(affine);
-                        final int srcDim = cornerToCRS.getSourceDimensions();       // Translation column in matrix.
-                        final int tgtDim = cornerToCRS.getTargetDimensions();       // Number of matrix rows before last row.
-                        for (int j=0; j<tgtDim; j++) {
-                            final DoubleDouble scale  = (DoubleDouble) affine.getNumber(j, j);
-                            final DoubleDouble offset = (DoubleDouble) affine.getNumber(j, srcDim);
-                            scale.multiply(0.5);
-                            offset.add(scale);
-                            affine.setNumber(j, srcDim, offset);
-                        }
-                        MathTransform gridToCRS = MathTransforms.linear(affine);
-
-                        //apply flipped axis
-                        final MatrixSIS flip = Matrices.createDiagonal(affine.getNumRow(), affine.getNumCol());
-                        for (Integer i : flippedAxis) {
-                            flip.setElement(i, i, -1);
-                            flip.setElement(i, srcDim, extent.getSize(i, true));
-                        }
-
-                        gridToCRS = MathTransforms.concatenate(MathTransforms.linear(flip), gridToCRS);
-
-                        domain = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, env.getCoordinateReferenceSystem());
-                    }
-                }
-            }
-        }
-        return domain;
+    private static String missingProperty(final String name) {
+        return Errors.format(Errors.Keys.MissingValueForProperty_1, name);
     }
 }
-
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 559b8ad..db09c1e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -252,7 +252,7 @@ public class GridExtent implements GridEnvelope, Serializable {
      * @param  width   number of pixels in each row.
      * @param  height  number of pixels in each column.
      */
-    GridExtent(final long xmin, final long ymin, final long width, final long height) {
+    GridExtent(final int xmin, final int ymin, final int width, final int height) {
         this(width, height);
         for (int i=coordinates.length; --i >= 0;) {
             coordinates[i] += ((i & 1) == 0) ? xmin : ymin;
@@ -659,8 +659,8 @@ public class GridExtent implements GridEnvelope, Serializable {
     /**
      * Returns the number of grid coordinates as a double precision floating point value.
      * Invoking this method is equivalent to invoking {@link #getSize(int)} and converting
-     * the result from {@code long} to the {@code double} primitive type,
-     * except that this method does not overflow.
+     * the result from {@code long} to the {@code double} primitive type, except that this
+     * method does not overflow (i.e. does not throw {@link ArithmeticException}).
      *
      * @param  index     the dimension for which to obtain the size.
      * @param  minusOne  {@code true} for returning <var>size</var>−1 instead of <var>size</var>.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 58c0445..9794c29 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -809,7 +809,7 @@ public class GridGeometry implements Serializable {
      * Returns the number of dimensions of the <em>CRS</em>. This is typically the same than the
      * number of {@linkplain #getDimension() grid dimensions}, but not necessarily.
      */
-    private int getTargetDimension() {
+    final int getTargetDimension() {
         if (envelope != null) {
             return envelope.getDimension();     // Most reliable source since that class is final.
         } else if (gridToCRS != null) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BufferedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BufferedGridCoverage.java
index a6e643c..505b432 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BufferedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BufferedGridCoverage.java
@@ -44,8 +44,12 @@ import org.opengis.coverage.CannotEvaluateException;
 /**
  * A {@link GridCoverage} with data stored in an in-memory Java2D buffer.
  * Those data can be shown as {@link RenderedImage}.
+ * Images are created when {@link #render(GridExtent)} is invoked instead than at construction time.
+ * This delayed construction makes this class better suited to <var>n</var>-dimensional grids since
+ * those grids can not be wrapped into a single {@link RenderedImage}.
  *
  * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.0
  * @module
@@ -60,7 +64,14 @@ public class BufferedGridCoverage extends GridCoverage {
 
     /**
      * Constructs a grid coverage using the specified grid geometry, sample dimensions and data buffer.
-     * This method stores the given buffer by reference (no copy).
+     * This method stores the given buffer by reference (no copy). The bands in the given buffer can be
+     * stored either in a single bank (pixel interleaved image) or in different banks (banded image).
+     * This class detects automatically which of those two sample models is used.
+     *
+     * <p>Note that {@link DataBuffer} does not contain any information about image size.
+     * Consequently {@link #render(GridExtent)} depends on the domain {@link GridExtent},
+     * which must be accurate. If the extent size does not reflect accurately the image size,
+     * then the image will not be rendered properly.</p>
      *
      * @param  domain  the grid extent, CRS and conversion from cell indices to CRS.
      * @param  range   sample dimensions for each image band.
@@ -87,11 +98,8 @@ public class BufferedGridCoverage extends GridCoverage {
          * Verify that the buffer has enough elements for all cells in grid extent.
          * Note that the buffer may have all elements in a single bank.
          */
-        long expectedSize = numBands;
         final GridExtent extent = domain.getExtent();
-        for (int i = extent.getDimension(); --i >= 0;) {
-            expectedSize = Math.multiplyExact(expectedSize, extent.getSize(i));
-        }
+        final long expectedSize = getSampleCount(extent, numBands);
         final long bufferSize = JDK9.multiplyFull(data.getSize(), numBanks);
         if (bufferSize < expectedSize) {
             final StringBuilder b = new StringBuilder();
@@ -116,12 +124,7 @@ public class BufferedGridCoverage extends GridCoverage {
      */
     public BufferedGridCoverage(final GridGeometry grid, final Collection<? extends SampleDimension> bands, final int dataType) {
         super(grid, bands);
-        long nbSamples = bands.size();
-        final GridExtent extent = grid.getExtent();
-        for (int i = grid.getDimension(); --i >= 0;) {
-            nbSamples = Math.multiplyExact(nbSamples, extent.getSize(i));
-        }
-        final int n = Math.toIntExact(nbSamples);
+        final int n = Math.toIntExact(getSampleCount(grid.getExtent(), bands.size()));
         switch (dataType) {
             case DataBuffer.TYPE_BYTE:   data = new DataBufferByte  (n); break;
             case DataBuffer.TYPE_SHORT:  data = new DataBufferShort (n); break;
@@ -134,6 +137,21 @@ public class BufferedGridCoverage extends GridCoverage {
     }
 
     /**
+     * Returns the number of cells in the given extent multiplied by the number of bands.
+     *
+     * @param  extent     the extent for which to get the number of cells.
+     * @param  nbSamples  number of bands.
+     * @return number of cells multiplied by the number of bands.
+     * @throws ArithmeticException if the number of samples exceeds 64-bits integer capacity.
+     */
+    private static long getSampleCount(final GridExtent extent, long nbSamples) {
+        for (int i = extent.getDimension(); --i >= 0;) {
+            nbSamples = Math.multiplyExact(nbSamples, extent.getSize(i));
+        }
+        return nbSamples;
+    }
+
+    /**
      * Returns a two-dimensional slice of grid data as a rendered image.
      * This method returns a view; sample values are not copied.
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index d5feb93..ce4d1b9 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -109,6 +109,23 @@ public final class ImageUtilities extends Static {
     }
 
     /**
+     * Returns the number of bands in the given image, or 0 if the image or its sample model is null.
+     *
+     * @param  image  the image for which to get the number of bands, or {@code null}.
+     * @return number of bands in the specified image, or 0 if the image or its sample model is null.
+     *
+     * @see SampleModel#getNumBands()
+     * @see Raster#getNumBands()
+     */
+    public static int getNumBands(final RenderedImage image) {
+        if (image != null) {
+            final SampleModel sm = image.getSampleModel();
+            if (sm != null) return sm.getNumBands();
+        }
+        return 0;
+    }
+
+    /**
      * If the given image is showing only one band, returns the index of that band.
      * Otherwise returns 0. Image showing only one band are SIS-specific (usually an
      * image show all its bands).
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
new file mode 100644
index 0000000..bf363d4
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
@@ -0,0 +1,181 @@
+/*
+ * 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.coverage.j2d;
+
+import java.awt.image.Raster;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import org.apache.sis.image.PlanarImage;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * A rendered image which can contain an arbitrary number of tiles. Tiles are stored in memory.
+ * This class may become public in a future version, but not yet because managing large tiled
+ * images would require a more sophisticated class than current implementation.
+ *
+ * @author  Rémi Maréchal (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class TiledImage extends PlanarImage {
+    /**
+     * The color model, or {@code null} if none.
+     */
+    private final ColorModel colorModel;
+
+    /**
+     * Number of pixels along X or Y axis in the whole rendered image.
+     */
+    private final int width, height;
+
+    /**
+     * Index of the first tile in the image. Should be a non-trivial value
+     * for increasing the chances to detect error in index calculation.
+     */
+    private final int minTileX, minTileY;
+
+    /**
+     * The tiles. They must all use the same sample model.
+     */
+    private final Raster[] tiles;
+
+    /**
+     * Creates a new tiled image. The first tile in the given array must be the
+     * one located at the minimal tile indices. All tiles must have the same size
+     * and the same sample model and must be sorted in row-major fashion
+     * (this is not verified in current version, but may be in the future).
+     *
+     * @param colorModel  the color model, or {@code null} if none.
+     * @param width       number of pixels along X axis in the whole rendered image.
+     * @param height      number of pixels along Y axis in the whole rendered image.
+     * @param minTileX    minimum tile index in the X direction.
+     * @param minTileY    minimum tile index in the Y direction.
+     * @param tiles       the tiles. Must contains at least one element.
+     */
+    public TiledImage(final ColorModel colorModel, final int width, final int height,
+                      final int minTileX, final int minTileY, final Raster... tiles)
+    {
+        ArgumentChecks.ensureStrictlyPositive("width",  width);
+        ArgumentChecks.ensureStrictlyPositive("height", height);
+        ArgumentChecks.ensureNonEmpty        ("tiles",  tiles);
+        this.colorModel = colorModel;
+        this.width      = width;
+        this.height     = height;
+        this.minTileX   = minTileX;
+        this.minTileY   = minTileY;
+        this.tiles      = tiles;
+    }
+
+    /**
+     * Returns the color model, or {@code null} if none.
+     */
+    @Override
+    public ColorModel getColorModel() {
+        return colorModel;
+    }
+
+    /**
+     * Returns the sample model.
+     */
+    @Override
+    public SampleModel getSampleModel() {
+        return tiles[0].getSampleModel();
+    }
+
+
+    /**
+     * Returns the minimum <var>x</var> coordinate (inclusive) of this image.
+     */
+    @Override
+    public int getMinX() {
+        return tiles[0].getMinX();
+    }
+
+    /**
+     * Returns the minimum <var>y</var> coordinate (inclusive) of this image.
+     */
+    @Override
+    public int getMinY() {
+        return tiles[0].getMinY();
+    }
+
+    /**
+     * Returns the number of pixels along X axis in the whole rendered image.
+     */
+    @Override
+    public int getWidth() {
+        return width;
+    }
+
+    /**
+     * Returns the number of pixels along Y axis in the whole rendered image.
+     */
+    @Override
+    public int getHeight() {
+        return height;
+    }
+
+    /**
+     * Returns the tile width in pixels. All tiles must have the same width.
+     */
+    @Override
+    public int getTileWidth() {
+        return tiles[0].getWidth();
+    }
+
+    /**
+     * Returns the tile height in pixels. All tiles must have the same height.
+     */
+    @Override
+    public int getTileHeight() {
+        return tiles[0].getHeight();
+    }
+
+    /**
+     * Returns the minimum tile index in the X direction.
+     */
+    @Override
+    public int getMinTileX() {
+        return minTileX;
+    }
+
+    /**
+     * Returns the minimum tile index in the Y direction.
+     */
+    @Override
+    public int getMinTileY() {
+        return minTileY;
+    }
+
+    /**
+     * Returns the tile at the given location in tile coordinates.
+     */
+    @Override
+    public Raster getTile(int tileX, int tileY) {
+        final int numXTiles = getNumXTiles();
+        final int numYTiles = getNumYTiles();
+        if ((tileX -= minTileX) < 0 || tileX >= numXTiles ||
+            (tileY -= minTileY) < 0 || tileY >= numYTiles)
+        {
+            throw new IndexOutOfBoundsException();
+        }
+        return tiles[tileX + tileY * numXTiles];
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
index 315979f..5a790bf 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
@@ -68,6 +68,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotAssignCharacteristics_1 = 2;
 
         /**
+         * Can not build the grid coverage.
+         */
+        public static final short CanNotBuildGridCoverage = 72;
+
+        /**
          * Can not compute tile ({0}, {1}).
          */
         public static final short CanNotComputeTile_2 = 66;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
index ef7e9b3..3cb001c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
@@ -21,6 +21,7 @@
 #
 AbstractFeatureType_1             = Feature type \u2018{0}\u2019 is abstract.
 CanNotAssignCharacteristics_1     = Can not assign characteristics to the \u201c{0}\u201d property.
+CanNotBuildGridCoverage           = Can not build the grid coverage.
 CanNotComputeTile_2               = Can not compute tile ({0}, {1}).
 CanNotProcessTile_2               = Can not process tile ({0}, {1}).
 CanNotUpdateTile_2                = Can not update tile ({0}, {1}).
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
index 7eb8dc9..324af81 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
@@ -26,6 +26,7 @@
 #
 AbstractFeatureType_1             = Le type d\u2019entit\u00e9 \u2018{0}\u2019 est abstrait.
 CanNotAssignCharacteristics_1     = Ne peut pas assigner des caract\u00e9ristiques \u00e0 la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb.
+CanNotBuildGridCoverage           = Ne peut pas construire la couverture de donn\u00e9es.
 CanNotComputeTile_2               = Ne peut pas calculer la tuile ({0}, {1}).
 CanNotProcessTile_2               = Ne peut pas traiter la tuile ({0}, {1}).
 CanNotUpdateTile_2                = Ne peut pas mettre \u00e0 jour la tuile ({0}, {1}).
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
index c2fcfb3..66a8904 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.awt.Dimension;
 import java.awt.image.BufferedImage;
 import java.awt.image.DataBuffer;
 import java.awt.image.DataBufferByte;
@@ -29,7 +30,6 @@ import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
-import org.apache.sis.util.NullArgumentException;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -57,7 +57,7 @@ public final strictfp class GridCoverageBuilderTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridCoverageBuilder#setValues(WritableRaster)}.
+     * Tests {@link GridCoverageBuilder#setValues(Raster)}.
      */
     @Test
     public void testBuildFromRaster() {
@@ -104,7 +104,7 @@ public final strictfp class GridCoverageBuilderTest extends TestCase {
             try {
                 builder.build();
                 fail("Wrong number of sample dimensions, build() should fail.");
-            } catch (IllegalArgumentException ex) {
+            } catch (IllegalStateException ex) {
                 assertNotNull(ex.getMessage());
             }
             final SampleDimension[] ranges = new SampleDimension[numBands];
@@ -144,7 +144,7 @@ public final strictfp class GridCoverageBuilderTest extends TestCase {
         try {
             builder.build();
             fail("Wrong extent size, build() should fail.");
-        } catch (IllegalArgumentException ex) {
+        } catch (IllegalStateException ex) {
             assertNotNull(ex.getMessage());
         }
         grid = new GridGeometry(new GridExtent(width, height), env);
@@ -153,19 +153,20 @@ public final strictfp class GridCoverageBuilderTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridCoverageBuilder#setValues(DataBuffer)}.
+     * Tests {@link GridCoverageBuilder#setValues(DataBuffer, Dimension)}.
      */
     @Test
     public void createFromBufferTest() {
         final DataBuffer buffer = new DataBufferByte(new byte[] {1,2,3,4,5,6}, 6);
         final GridCoverageBuilder builder = new GridCoverageBuilder();
-        assertSame(builder, builder.setValues(buffer));
         assertSame(builder, builder.setRanges(new SampleDimension.Builder().setName(0).build()));
+        assertSame(builder, builder.setValues(buffer, null));
         try {
             builder.build();
             fail("Extent is undefined, build() should fail.");
-        } catch (NullArgumentException ex) {
-            assertNotNull(ex.getMessage());
+        } catch (IncompleteGridGeometryException ex) {
+            final String message = ex.getMessage();         // "No value for "size" property" but may be localized.
+            assertTrue(message, message.contains("size"));
         }
         final GridCoverage coverage = testSetDomain(builder, 3, 2);
         assertSame(buffer, coverage.render(null).getTile(0,0).getDataBuffer());
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
index 4638060..4be8aaf 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
@@ -430,7 +430,7 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition> {
      */
     private void negate(final int dimension) {
         if (dimension >= Long.SIZE) {
-            throw new ArithmeticException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension));
+            throw new ArithmeticException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension + 1));
         }
         negate |= (1L << dimension);
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
index 3a95a25..08e36c3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
@@ -417,7 +417,7 @@ public final class Envelopes extends Static {
          * This coordinate will be updated in the `switch` statement inside the `while` loop.
          */
         if (sourceDim >= 20) {          // Maximal value supported by Formulas.pow3(int) is 19.
-            throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1));
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, sourceDim));
         }
         int             pointIndex            = 0;
         boolean         isDerivativeSupported = true;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index 4e2380c..64fc85e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@ -958,7 +958,7 @@ public final class CRS extends Static {
                 throw new IndexOutOfBoundsException(Errors.format(Errors.Keys.IndexOutOfBounds_1, d));
             }
             if (d >= Long.SIZE) {
-                throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, d));
+                throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, d+1));
             }
             selected |= (1L << d);
         }


Mime
View raw message