sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Partial review of GridCoverage.evaluate(DirectPosition) method. - Add a new FractionalGridCoordinates class with some of the calculations done by GridCoverage methods. - Rename toGridCoord as toGridCoordinates and change the return type to FractionalGridCoordinates. - Remove toLongExact(DirectPosition), replaced by FractionalGridCoordinates.
Date Wed, 18 Dec 2019 19:07:27 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 0431f2e9f297cab118d4c31a6fb7cb24b5fe2e23
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Dec 18 14:38:22 2019 +0100

    Partial review of GridCoverage.evaluate(DirectPosition) method.
    - Add a new FractionalGridCoordinates class with some of the calculations done by GridCoverage methods.
    - Rename toGridCoord as toGridCoordinates and change the return type to FractionalGridCoordinates.
    - Remove toLongExact(DirectPosition), replaced by FractionalGridCoordinates.
---
 .../coverage/grid/FractionalGridCoordinates.java   | 473 +++++++++++++++++++++
 .../sis/coverage/grid/GridCoordinatesView.java     |  10 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java | 191 +++++----
 .../org/apache/sis/coverage/grid/GridExtent.java   |  71 ++--
 .../org/apache/sis/coverage/grid/GridGeometry.java |   4 +-
 .../sis/coverage/grid/PointToGridCoordinates.java  |  81 ++++
 .../sis/internal/coverage/GridCoverage2D.java      | 232 ++++++----
 .../org/apache/sis/internal/feature/Resources.java |   5 +
 .../sis/internal/feature/Resources.properties      |   1 +
 .../sis/internal/feature/Resources_fr.properties   |   1 +
 .../grid/FractionalGridCoordinatesTest.java        | 101 +++++
 .../coverage/BufferedGridCoverageTest.java         |   3 +-
 .../sis/internal/coverage/GridCoverage2DTest.java  |   3 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 14 files changed, 956 insertions(+), 221 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
new file mode 100644
index 0000000..d7df49c
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
@@ -0,0 +1,473 @@
+/*
+ * 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.Arrays;
+import java.io.Serializable;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.coverage.grid.GridCoordinates;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.PixelInCell;
+import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.internal.util.Strings;
+import org.apache.sis.util.StringBuilders;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Grid coordinates which may have fraction digits after the integer part.
+ * Grid coordinates specify the location of a cell within a {@link GridCoverage}.
+ * They are normally integer numbers, but fractional parts may exist for example
+ * after converting a geospatial {@link DirectPosition} to grid coordinates.
+ * Preserving that fractional part is sometime useful, e.g. for interpolations.
+ * This class can store such fractional part and can also compute a {@link GridExtent}
+ * containing the coordinates, which can be used for requesting data for interpolations.
+ *
+ * <p>Current implementation stores coordinate values as {@code double} precision floating-point numbers
+ * and {@linkplain Math#round(double) rounds} them to 64-bits integers on the fly. If a {@code double}
+ * can not be {@linkplain #getCoordinateValue(int) returned} as a {@code long}, or if a {@code long}
+ * can not be {@linkplain #setCoordinateValue(int, long) stored} as a {@code double}, then an
+ * {@link ArithmeticException} is thrown.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @see GridCoverage#toGridCoordinates(DirectPosition)
+ *
+ * @since 1.1
+ * @module
+ */
+public class FractionalGridCoordinates implements GridCoordinates, Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 5652265407347129550L;
+
+    /**
+     * The grid coordinates as floating-point numbers.
+     */
+    final double[] coordinates;
+
+    /**
+     * Creates a new grid coordinates with the given number of dimensions.
+     *
+     * <div class="note"><b>Note:</b>
+     * {@code FractionalGridCoordinates} are usually not created directly, but are instead obtained
+     * indirectly for example from the {@linkplain GridCoverage#toGridCoordinates(DirectPosition)
+     * conversion of a geospatial position}.</div>
+     *
+     * @param  dimension  the number of dimensions.
+     */
+    public FractionalGridCoordinates(final int dimension) {
+        coordinates = new double[dimension];
+    }
+
+    /**
+     * Creates a new grid coordinates initialized to a copy of the given coordinates.
+     *
+     * @param  other  the coordinates to copy.
+     */
+    public FractionalGridCoordinates(final FractionalGridCoordinates other) {
+        coordinates = other.coordinates.clone();
+    }
+
+    /**
+     * Returns the number of dimension of this grid coordinates.
+     *
+     * @return  the number of dimensions.
+     */
+    @Override
+    public int getDimension() {
+        return coordinates.length;
+    }
+
+    /**
+     * Returns one integer value for each dimension of the grid.
+     * The default implementation invokes {@link #getCoordinateValue(int)}
+     * for each element in the returned array.
+     *
+     * @return a copy of the coordinates. Changes in the returned array will
+     *         not be reflected back in this {@code GridCoordinates} object.
+     * @throws ArithmeticException if a coordinate value is outside the range
+     *         of values representable as a 64-bits integer value.
+     */
+    @Override
+    public long[] getCoordinateValues() {
+        final long[] indices = new long[coordinates.length];
+        for (int i=0; i<indices.length; i++) {
+            indices[i] = getCoordinateValue(i);
+        }
+        return indices;
+    }
+
+    /**
+     * Returns the grid coordinate value at the specified dimension.
+     * Floating-point values are rounded to the nearest 64-bits integer values.
+     * If the coordinate value is NaN or outside the range of {@code long} values,
+     * then an {@link ArithmeticException} is thrown.
+     *
+     * @param  dimension  the dimension for which to obtain the coordinate value.
+     * @return the coordinate value at the given dimension,
+     *         {@linkplain Math#round(double) rounded} to nearest integer.
+     * @throws IndexOutOfBoundsException if the given index is negative or is
+     *         equal or greater than the {@linkplain #getDimension grid dimension}.
+     * @throws ArithmeticException if the coordinate value is outside the range
+     *         of values representable as a 64-bits integer value.
+     */
+    @Override
+    public long getCoordinateValue(final int dimension) {
+        final double value = coordinates[dimension];
+        /*
+         * 2048 is the smallest value than can be added or removed to Long.MIN/MAX_VALUE,
+         * as given by Math.ulp(Long.MIN_VALUE). We add this tolerance since the contract
+         * is to return the `long` value closest to the `double` value and we consider a
+         * 1 ULP error as close enough.
+         */
+        if (value >= (Long.MIN_VALUE - 2048d) && value <= (Long.MAX_VALUE + 2048d)) {
+            return Math.round(value);
+        }
+        throw new ArithmeticException(Resources.format(Resources.Keys.UnconvertibleGridCoordinate_2, "long", value));
+    }
+
+    /**
+     * Returns a grid coordinate value together with its fractional part, if any.
+     *
+     * @param  dimension  the dimension for which to obtain the coordinate value.
+     * @return the coordinate value at the given dimension.
+     * @throws IndexOutOfBoundsException if the given index is negative or is
+     *         equal or greater than the {@linkplain #getDimension grid dimension}.
+     */
+    public double getCoordinateFractional(final int dimension) {
+        return coordinates[dimension];
+    }
+
+    /**
+     * Sets the coordinate value at the specified dimension. The given value can not be
+     * NaN or infinite and shall be convertible to {@code long} without precision lost.
+     *
+     * @param  dimension  the dimension for which to set the coordinate value.
+     * @param  value      the new value.
+     * @throws IndexOutOfBoundsException if the given index is negative or is
+     *         equal or greater than the {@linkplain #getDimension grid dimension}.
+     * @throws ArithmeticException if this method can not store the given grid coordinate
+     *         without precision lost.
+     */
+    @Override
+    public void setCoordinateValue(final int dimension, final long value) {
+        if ((coordinates[dimension] = value) != value) {
+            throw new ArithmeticException(Resources.format(Resources.Keys.UnconvertibleGridCoordinate_2, "double", value));
+        }
+    }
+
+    /**
+     * Creates a new grid extent around this grid coordinates. The returned extent will have the same number
+     * of dimensions than this grid coordinates. For each dimension <var>i</var> the following relationships
+     * will hold:
+     *
+     * <ol>
+     *   <li>If <code>extent.{@linkplain GridExtent#getSize(int) getSize}(i)</code> ≥ 2 and no shift (see below) then:<ul>
+     *      <li><code>extent.{@linkplain GridExtent#getLow(int)  getLow}(i)</code> ≤
+     *          <code>{@linkplain #getCoordinateFractional(int)  getCoordinateFractional}(i)</code></li>
+     *      <li><code>extent.{@linkplain GridExtent#getHigh(int) getHigh}(i)</code> ≥
+     *          <code>{@linkplain #getCoordinateFractional(int)  getCoordinateFractional}(i)</code></li>
+     *   </ul></li>
+     *   <li>If {@code bounds.getSize(i)} ≥ {@code size[i]} and {@code size[i]} ≠ 0 then:<ul>
+     *      <li><code>extent.{@linkplain GridExtent#getSize(int) getSize}(i)</code> = {@code size[i]}</li>
+     *   </ul></li>
+     * </ol>
+     *
+     * <p>The {@code size} argument is optional and can be incomplete (i.e. the number of {@code size} values can be
+     * less than the number of dimensions). For each dimension <var>i</var>, if a {@code size[i]} value is provided
+     * and is not zero, then this method tries to expand the extent in that dimension to the specified {@code size[i]}
+     * value as shown in constraint #2 above. Otherwise the default size is the smallest possible extent that met
+     * constraint #1 above, clipped to the {@code bounds}. This implies a size of 1 if the grid coordinate in that
+     * dimension is an integer, or a size of 2 (before clipping to the bounds) if the grid coordinate has a fractional
+     * part.</p>
+     *
+     * <p>The {@code bounds} argument is also optional.
+     * If non-null, then this method enforces the following additional rules:</p>
+     *
+     * <ul>
+     *   <li>Coordinates must be inside the given bounds, otherwise an {@link DisjointExtentException} is thrown.</li>
+     *   <li>If the computed extent overlaps an area outside the bounds, then the extent will be shifted (if an explicit
+     *       size was given) or clipped (if default size is used) in order to be be fully contained inside the bounds.</li>
+     *   <li>If a given size is larger than the corresponding bounds {@linkplain GridExtent#getSize(int) size},
+     *       then the returned extent will be clipped to the bounds.</li>
+     * </ul>
+     *
+     * <p>In all cases, this method tries to keep the grid coordinates close to the center of the returned extent.
+     * A shift may exist if necessary for keeping the extent inside the {@code bounds} argument, but will never
+     * move the grid coordinates outside the [<var>low</var> … <var>high</var>+1) range of returned extent.</p>
+     *
+     * @param  bounds  if the coordinates shall be contained inside a grid, that grid. Otherwise {@code null}.
+     * @param  size    the desired extent sizes as strictly positive numbers, or 0 sentinel values for automatic
+     *                 sizes (1 or 2 depending on bounds and coordinate values). This array may have any length;
+     *                 if shorter than the number of dimensions, missing values default to 0.
+     *                 If longer than the number of dimensions, extra values are ignored.
+     * @throws IllegalArgumentException if a {@code size} value is negative.
+     * @throws ArithmeticException if a coordinate value is outside the range of {@code long} values.
+     * @throws MismatchedDimensionException if {@code bounds} dimension is not equal to grid coordinates dimension.
+     * @throws DisjointExtentException if the returned extent would not intersect the given bounds.
+     * @return a grid extent of the given size (if possible) containing those grid coordinates.
+     */
+    public GridExtent toExtent(final GridExtent bounds, final long... size) {
+        final int dimension = coordinates.length;
+        if (bounds != null) {
+            final int bd = bounds.getDimension();
+            if (bd != dimension) {
+                throw new MismatchedDimensionException(Errors.format(
+                        Errors.Keys.MismatchedDimension_3, "bounds", dimension, bd));
+            }
+        }
+        final long[] extent = GridExtent.allocate(dimension);
+        for (int i=0; i<dimension; i++) {
+            final double value = coordinates[i];
+            if (!(value >= Long.MIN_VALUE && value <= Long.MAX_VALUE)) {        // Use ! for catching NaN values.
+                throw new ArithmeticException(Resources.format(
+                        Resources.Keys.UnconvertibleGridCoordinate_2, "long", value));
+            }
+            long margin = 0;
+            if (i < size.length) {
+                margin = size[i];
+                if (margin < 0) {
+                    throw new IllegalArgumentException(Errors.format(
+                            Errors.Keys.NegativeArgument_2, Strings.toIndexed("size", i), margin));
+                }
+            }
+            /*
+             * The lower/upper values are given by Math.floor/ceil respectively (may be equal).
+             * However we do an exception to this rule if user asked explicitly for a size of 1.
+             * In such case we can no longer enforce the `lower ≤ value ≤ upper` rule. The best
+             * we can do is to take the nearest neighbor.
+             */
+            long lower, upper;
+            if (margin == 1) {
+                lower = upper = Math.round(value);
+            } else {
+                final double base = Math.floor(value);
+                lower = (long) base;                    // Inclusive.
+                upper = (long) Math.ceil(value);        // Inclusive too (lower == upper if value is an integer).
+                if (margin != 0) {
+                    margin -= (upper - lower + 1);
+                    assert margin >= 0 : margin;        // Because (upper - lower + 1) ≤ 2
+                    if ((margin & 1) != 0) {
+                        if (value - base >= 0.5) {
+                            upper = Math.incrementExact(upper);
+                        } else {
+                            lower = Math.decrementExact(lower);
+                        }
+                    }
+                    margin /= 2;
+                    lower  = Math.subtractExact(lower, margin);
+                    upper  = Math.addExact(upper, margin);
+                    margin = 2;       // Any value different than 0 for remembering that it was explicitly specified.
+                }
+            }
+            /*
+             * At this point the grid range has been computed (lower to upper).
+             * Shift it if needed for keeping it inside the enclosing extent.
+             */
+            if (bounds != null) {
+                final long validMin = bounds.getLow(i);
+                final long validMax = bounds.getHigh(i);
+                if (lower > validMax || upper < validMin) {
+                    throw new DisjointExtentException(bounds.getAxisIdentification(i,i), validMin, validMax, lower, upper);
+                }
+                if (upper > validMax) {
+                    if (margin != 0) {      // In automatic mode (margin = 0) just clip, don't shift.
+                        /*
+                         * Because (upper - validMax) is always positive, then (t > lower) would mean
+                         * that we have an overflow. In such cases we do not need the result since we
+                         * know that we are outside the enclosing extent anyway.
+                         */
+                        final long t = lower - Math.subtractExact(upper, validMax);
+                        lower = (t >= validMin && t <= lower) ? t : validMin;
+                    }
+                    upper = validMax;
+                }
+                if (lower < validMin) {
+                    if (margin != 0) {
+                        final long t = upper + Math.subtractExact(validMin, lower);
+                        upper = (t <= validMax && t >= upper) ? t : validMax;           // Same rational than above.
+                    }
+                    lower = validMin;
+                }
+            }
+            extent[i] = lower;
+            extent[i+dimension] = upper;
+        }
+        return new GridExtent(bounds, extent);
+    }
+
+    /**
+     * Creates a new grid coordinates computed from the given geospatial position. The given {@code crsToGrid}
+     * argument should be the inverse of {@link GridGeometry#getGridToCRS(PixelInCell)}. This method does not
+     * verify if CRS of the given point and does not verify if the resulting grid coordinates are inside the
+     * grid coverage bounds.
+     *
+     * @param  point       the geospatial position.
+     * @param  crsToGrid   conversion from the geospatial CRS to the grid coordinates.
+     * @return the given position converted to grid coordinates (possibly out of grid bounds).
+     * @throws TransformException if the given position can not be converted.
+     *
+     * @see GridCoverage#toGridCoordinates(DirectPosition)
+     */
+    static FractionalGridCoordinates fromPosition(final DirectPosition point, final MathTransform crsToGrid)
+            throws TransformException
+    {
+        final Position gc = new Position(crsToGrid.getTargetDimensions());
+        final DirectPosition result = crsToGrid.transform(point, gc);
+        if (result != gc) {
+            final double[] coordinates = result.getCoordinate();
+            System.arraycopy(coordinates, 0, gc.coordinates, 0, gc.coordinates.length);
+        }
+        return gc;
+    }
+
+    /**
+     * Returns the grid coordinates converted to a geospatial position using the given transform.
+     * The {@code gridToCRS} argument is typically {@link GridGeometry#getGridToCRS(PixelInCell)}
+     * with {@link PixelInCell#CELL_CENTER}.
+     *
+     * @param  gridToCRS  the transform to apply on grid coordinates.
+     * @return the grid coordinates converted using the given transform.
+     * @throws TransformException if the grid coordinates can not be converted by {@code gridToCRS}.
+     *
+     * @see GridCoverage#toGridCoordinates(DirectPosition)
+     */
+    public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
+        return gridToCRS.transform(new Position(this), null);
+    }
+
+    /**
+     * A grid coordinates viewed as a {@link DirectPosition}. This class is used only for coordinate transformation.
+     * We do not want to make this class public in order to avoid the abuse of {@link DirectPosition} as a storage
+     * of grid coordinates.
+     *
+     * <p>Note this this class does not comply with the contract documented in {@link DirectPosition#equals(Object)}
+     * and {@link DirectPosition#hashCode()} javadoc. This is another reason for not making this class public.</p>
+     */
+    private static final class Position extends FractionalGridCoordinates implements DirectPosition {
+        /**
+         * For cross-version compatibility.
+         */
+        private static final long serialVersionUID = -7804151694395153401L;
+
+        /**
+         * Creates a new position of the given number of dimensions.
+         */
+        Position(final int dimension) {
+            super(dimension);
+        }
+
+        /**
+         * Creates a new position initialized to a copy of the given coordinates.
+         */
+        Position(final FractionalGridCoordinates other) {
+            super(other);
+        }
+
+        /**
+         * Returns the direct position, which is this object itself.
+         */
+        @Override
+        public DirectPosition getDirectPosition() {
+            return this;
+        }
+
+        /**
+         * Grid coordinates have no coordinate reference system.
+         */
+        @Override
+        public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+            return null;
+        }
+
+        /**
+         * Returns all coordinate values.
+         */
+        @Override
+        public double[] getCoordinate() {
+            return coordinates.clone();
+        }
+
+        /**
+         * Returns the coordinate value at the given dimension.
+         */
+        @Override
+        public double getOrdinate(int dimension) {
+            return coordinates[dimension];
+        }
+
+        /**
+         * Sets the coordinate value at the given dimension.
+         */
+        @Override
+        public void setOrdinate(final int dimension, final double value) {
+            coordinates[dimension] = value;
+        }
+
+        /**
+         * Returns the grid coordinates converted to a geospatial position using the given transform.
+         */
+        @Override
+        public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
+            return gridToCRS.transform(this, null);
+        }
+    }
+
+    /**
+     * Returns a string representation of this grid coordinates for debugging purpose.
+     */
+    @Override
+    public String toString() {
+        final StringBuilder buffer = new StringBuilder("GridCoordinates[");
+        for (int i=0; i<coordinates.length; i++) {
+            if (i != 0) buffer.append(' ');
+            StringBuilders.trimFractionalPart(buffer.append(coordinates[i]));
+        }
+        return buffer.append(']').toString();
+    }
+
+    /**
+     * Returns a hash code value for this grid coordinates.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(coordinates) ^ (int) serialVersionUID;
+    }
+
+    /**
+     * Compares this grid coordinates with the specified object for equality.
+     *
+     * @param  object  the object to compares with this grid coordinates.
+     * @return {@code true} if the given object is equal to this grid coordinates.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object == this) {                           // Slight optimization.
+            return true;
+        }
+        if (object != null && object.getClass() == getClass()) {
+            return Arrays.equals(((FractionalGridCoordinates) object).coordinates, coordinates);
+        }
+        return false;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
index efbb0ab..cc648b3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
@@ -27,7 +27,7 @@ import org.apache.sis.util.ArgumentChecks;
  * This is not a general-purpose grid coordinates since it assumes a {@link GridExtent} coordinates layout.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -89,7 +89,13 @@ final class GridCoordinatesView implements GridCoordinates {
      */
     @Override
     public final String toString() {
-        return "GridCoordinates".concat(Arrays.toString(getCoordinateValues()));
+        final StringBuilder buffer = new StringBuilder("GridCoordinates[");
+        final int dimension = getDimension();
+        for (int i=0; i<dimension; i++) {
+            if (i != 0) buffer.append(' ');
+            buffer.append(coordinates[i + offset]);
+        }
+        return buffer.append(']').toString();
     }
 
     /**
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 6da4620..1f182d3 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
@@ -19,6 +19,7 @@ package org.apache.sis.coverage.grid;
 import java.util.List;
 import java.util.Collection;
 import java.util.Locale;
+import java.util.Objects;
 import java.awt.image.RenderedImage;
 import org.opengis.coverage.PointOutsideCoverageException;
 import org.opengis.geometry.DirectPosition;
@@ -26,13 +27,10 @@ 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.opengis.util.FactoryException;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.image.PixelIterator;
-import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTable;
@@ -53,7 +51,7 @@ import org.opengis.coverage.CannotEvaluateException;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 2.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -75,18 +73,27 @@ public abstract class GridCoverage {
     private final SampleDimension[] sampleDimensions;
 
     /**
+     * The last coordinate operation used by {@link #toGridCoordinates(DirectPosition)}.
+     * This is cached for avoiding the costly process of fetching a coordinate operation
+     * in the common case where the coordinate reference systems did not changed.
+     */
+    private transient volatile PointToGridCoordinates lastConverter;
+
+    /**
      * Constructs a grid coverage using the specified grid geometry and sample dimensions.
+     * The grid geometry defines the "domain" (inputs) of the coverage function,
+     * and the sample dimensions define the "range" (output) of that function.
      *
-     * @param grid   the grid extent, CRS and conversion from cell indices to CRS.
-     * @param bands  sample dimensions for each image band.
+     * @param  domain  the grid extent, CRS and conversion from cell indices to CRS.
+     * @param  range   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()]);
+    protected GridCoverage(final GridGeometry domain, final Collection<? extends SampleDimension> range) {
+        ArgumentChecks.ensureNonNull("domain", domain);
+        ArgumentChecks.ensureNonNull("range",  range);
+        gridGeometry = domain;
+        sampleDimensions = range.toArray(new SampleDimension[range.size()]);
         for (int i=0; i<sampleDimensions.length; i++) {
-            ArgumentChecks.ensureNonNullElement("bands", i, sampleDimensions[i]);
+            ArgumentChecks.ensureNonNullElement("range", i, sampleDimensions[i]);
         }
     }
 
@@ -159,6 +166,85 @@ public abstract class GridCoverage {
     public abstract GridCoverage forConvertedValues(boolean converted);
 
     /**
+     * Returns a sequence of double values for a given point in the coverage.
+     * The CRS of the given point may be any coordinate reference system;
+     * coordinate transformations will be applied as needed.
+     * If the CRS of the point is undefined, then it is assumed to be the same as this coverage.
+     * The returned sequence includes a value for each {@linkplain SampleDimension sample dimension}.
+     *
+     * <p>The default interpolation type used when accessing grid values for points which fall between
+     * grid cells is nearest neighbor. This default interpolation method may change in future version.</p>
+     *
+     * <p>The default implementation invokes {@link #render(GridExtent)} for a small region around the point.
+     * Subclasses should override with more efficient implementation.</p>
+     *
+     * <p>Warning: this method may change. See <a href="https://issues.apache.org/jira/browse/SIS-485">SIS-485</a>.</p>
+     *
+     * @param  point   the coordinate point where to evaluate.
+     * @param  buffer  an array in which to store values, or {@code null} to create a new array.
+     * @return the {@code buffer} array, or a newly created array if {@code buffer} was null.
+     * @throws PointOutsideCoverageException if the evaluation failed because the input point
+     *         has invalid coordinates.
+     * @throws CannotEvaluateException if the values can not be computed at the specified coordinate
+     *         for an other reason. It may be thrown if the coverage data type can not be converted
+     *         to {@code double} by an identity or widening conversion. Subclasses may relax this
+     *         constraint if appropriate.
+     *
+     * @since 1.1
+     */
+    public double[] evaluate(final DirectPosition point, double[] buffer) throws CannotEvaluateException {
+        /*
+         * TODO: instead of restricting to a single point, keep the automatic size (1 or 2),
+         * invoke render for each plan, then interpolate. We would keep a value of 1 in the
+         * size array if we want to disable interpolation in some particular axis (e.g. time).
+         */
+        final long[] size = new long[gridGeometry.getDimension()];
+        java.util.Arrays.fill(size, 1);
+        try {
+            final GridExtent subExtent = toGridCoordinates(point).toExtent(gridGeometry.extent, size);
+            final RenderedImage image = render(subExtent);
+            final PixelIterator ite = PixelIterator.create(image);  // TODO: avoid costly creation of PixelIterator here.
+            ite.moveTo(0, 0);
+            return ite.getPixel(buffer);
+        } catch (ArithmeticException | DisjointExtentException ex) {
+            throw (PointOutsideCoverageException) new PointOutsideCoverageException(ex.getMessage(), point).initCause(ex);
+        } catch (IllegalArgumentException | TransformException ex) {
+            throw new CannotEvaluateException(ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * Converts the specified geospatial position to grid coordinates. If the given position
+     * is associated to a non-null coordinate reference system (CRS) different than the CRS
+     * of this coverage, then this method automatically transforms that position to the
+     * {@linkplain #getCoordinateReferenceSystem() coverage CRS} before to compute grid coordinates.
+     *
+     * <p>This method does not put any restriction on the grid coordinates result.
+     * The result may be outside the {@linkplain GridGeometry#getExtent() grid extent}
+     * if the {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform allows it.</p>
+     *
+     * @param  point  geospatial coordinates (in arbitrary CRS) to transform to grid coordinates.
+     * @return the grid coordinates for the given geospatial coordinates.
+     * @throws IncompleteGridGeometryException if the {@linkplain #getGridGeometry() grid geometry}
+     *         does not define a "grid to CRS" transform, or if the given point has a non-null CRS
+     *         but this coverage does not {@linkplain #getCoordinateReferenceSystem() have a CRS}.
+     * @throws TransformException if the given coordinates can not be transformed.
+     *
+     * @see FractionalGridCoordinates#toPosition(MathTransform)
+     *
+     * @since 1.1
+     */
+    public FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException {
+        final CoordinateReferenceSystem sourceCRS = point.getCoordinateReferenceSystem();
+        PointToGridCoordinates converter = lastConverter;
+        if (converter == null || !Objects.equals(converter.sourceCRS, sourceCRS)) {
+            converter = new PointToGridCoordinates(sourceCRS, this, getGridGeometry());
+            lastConverter = converter;
+        }
+        return FractionalGridCoordinates.fromPosition(point, converter.crsToGrid);
+    }
+
+    /**
      * Returns a two-dimensional slice of grid data as a rendered image. The given {@code sliceExtent} argument specifies
      * the coordinates of the slice in all dimensions that are not in the two-dimensional image. For example if this grid
      * coverage has <i>(<var>x</var>,<var>y</var>,<var>z</var>,<var>t</var>)</i> dimensions and we want to render an image
@@ -234,38 +320,6 @@ public abstract class GridCoverage {
     public abstract RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException;
 
     /**
-     * Returns a sequence of double values for a given point in the coverage. A value for each
-     * {@linkplain SampleDimension sample dimension} is included in the sequence. The default
-     * interpolation type used when accessing grid values for points which fall between grid cells
-     * is nearest neighbor.
-     * The CRS of the point may be in any coordinate reference system.
-     * If the CRS of the point is undefined, it is assumed to be the same as the coverage.
-     *
-     * @param  coord The coordinate point where to evaluate.
-     * @param  dest An array in which to store values, or {@code null} to create a new array.
-     * @return The {@code dest} array, or a newly created array if {@code dest} was null.
-     * @throws PointOutsideCoverageException if the evaluation failed because the input point
-     *         has invalid coordinates.
-     * @throws CannotEvaluateException if the values can't be computed at the specified coordinate
-     *         for an other reason. It may be thrown if the coverage data type can't be converted
-     *         to {@code double} by an identity or widening conversion. Subclasses may relax this
-     *         constraint if appropriate.
-     */
-    public double[] evaluate(DirectPosition coord, double[] dest) throws CannotEvaluateException {
-        try {
-            coord = toGridCoord(coord);
-            final long[] coordl = toLongExact(coord);
-            final GridExtent subExtent = new GridExtent(null, coordl, coordl, true);
-            final RenderedImage image = render(subExtent);
-            final PixelIterator ite = PixelIterator.create(image);
-            ite.moveTo(0, 0);
-            return ite.getPixel(dest);
-        } catch (FactoryException | TransformException ex) {
-            throw new CannotEvaluateException(ex.getMessage(), ex);
-        }
-    }
-
-    /**
      * Returns a string representation of this grid coverage for debugging purpose.
      * The returned string is implementation dependent and may change in any future version.
      * Current implementation is equivalent to the following, where {@code <default flags>}
@@ -309,53 +363,4 @@ public abstract class GridCoverage {
         branch.newChild().setValue(column, SampleDimension.toString(locale, sampleDimensions));
         return tree;
     }
-
-    /**
-     * Converts the specified point to grid coordinate.
-     *
-     * @param point point to transform to grid coordinate
-     * @return point in grid coordinate
-     * @throws org.opengis.util.FactoryException if creating transformation fails
-     * @throws org.opengis.referencing.operation.TransformException if transformation fails
-     */
-    protected DirectPosition toGridCoord(final DirectPosition point)
-            throws FactoryException, TransformException
-    {
-        final CoordinateReferenceSystem sourceCRS = point.getCoordinateReferenceSystem();
-        MathTransform trs = getGridGeometry().getGridToCRS(PixelInCell.CELL_CENTER).inverse();
-        if (sourceCRS != null) {
-            MathTransform toCrs = CRS.findOperation(sourceCRS, getCoordinateReferenceSystem(), null).getMathTransform();
-            if (!toCrs.isIdentity()) {
-                trs = MathTransforms.concatenate(toCrs, trs);
-            }
-        }
-        return trs.transform(point, null);
-    }
-
-    /**
-     * Converts given grid coordinate to long values and ensure coordinate
-     * is inside grid geometry extent.
-     *
-     * @param position in grid coordinate
-     * @return position as long type in grid coordinate
-     * @throws PointOutsideCoverageException
-     */
-    protected long[] toLongExact(DirectPosition position) throws PointOutsideCoverageException {
-        final long[] coord = new long[position.getDimension()];
-        final GridExtent extent = getGridGeometry().getExtent();
-        final long[] low = extent.getLow().getCoordinateValues();
-        final long[] high = extent.getHigh().getCoordinateValues();
-
-        for (int i = 0; i < coord.length; i++) {
-            final double dv = position.getOrdinate(i);
-            if (!Double.isFinite(dv)) {
-                throw new PointOutsideCoverageException("Position outside coverage, axis " + i + " value " + dv);
-            }
-            coord[i] = Math.round(dv);
-            if (coord[i] < low[i] || coord[i] > high[i]) {
-                throw new PointOutsideCoverageException("Position outside coverage, axis " + i + " value " + coord[i]);
-            }
-        }
-        return coord;
-    }
 }
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 b2fa4ec..51529ef 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
@@ -78,7 +78,7 @@ import org.opengis.coverage.PointOutsideCoverageException;
  * The same instance can be shared by different {@link GridGeometry} instances.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -140,7 +140,7 @@ public class GridExtent implements GridEnvelope, Serializable {
      *
      * @throws IllegalArgumentException if the given number of dimensions is excessive.
      */
-    private static long[] allocate(final int dimension) throws IllegalArgumentException {
+    static long[] allocate(final int dimension) throws IllegalArgumentException {
         if (dimension >= Numerics.MAXIMUM_MATRIX_SIZE) {
             // Actually the real limit is Integer.MAX_VALUE / 2, but a value too high is likely to be an error.
             throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension));
@@ -323,6 +323,33 @@ public class GridExtent implements GridEnvelope, Serializable {
     {
         final int dimension = envelope.getDimension();
         coordinates = (enclosing != null) ? enclosing.coordinates.clone() : allocate(dimension);
+        /*
+         * Assign the `types` field before we try to compute the grid extent coordinates
+         * because if the coordinate computation fail, `getAxisIdentification(…)` uses
+         * that information for producing a more informative error message if possible.
+         */
+        if (enclosing != null && enclosing.types != null) {
+            types = enclosing.types;
+        } else {
+            DimensionNameType[] axisTypes = null;
+            final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
+            if (crs != null) {
+                final CoordinateSystem cs = crs.getCoordinateSystem();
+                for (int i=0; i<dimension; i++) {
+                    final DimensionNameType type = AXIS_DIRECTIONS.get(AxisDirections.absolute(cs.getAxis(i).getDirection()));
+                    if (type != null) {
+                        if (axisTypes == null) {
+                            axisTypes = new DimensionNameType[dimension];
+                        }
+                        axisTypes[i] = type;
+                    }
+                }
+            }
+            types = validateAxisTypes(axisTypes);
+        }
+        /*
+         * Now computes the grid extent coordinates.
+         */
         for (int i=0; i<dimension; i++) {
             double min = envelope.getLower(i);
             double max = envelope.getUpper(i);
@@ -418,37 +445,27 @@ public class GridExtent implements GridEnvelope, Serializable {
                 if (lower > validMin) coordinates[lo] = lower;
                 if (upper < validMax) coordinates[hi] = upper;
                 if (lower > validMax || upper < validMin) {
-                    throw new DisjointExtentException(enclosing.getAxisIdentification(lo, i), validMin, validMax, lower, upper);
+                    throw new DisjointExtentException(getAxisIdentification(lo, i), validMin, validMax, lower, upper);
                 }
             } else {
                 coordinates[i]   = lower;
                 coordinates[i+m] = upper;
             }
         }
-        /*
-         * At this point we finished to compute coordinate values.
-         * Now try to infer dimension types from the CRS axes.
-         * This is only for information purpose.
-         */
-        if (enclosing != null) {
-            types = enclosing.types;
-        } else {
-            DimensionNameType[] axisTypes = null;
-            final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
-            if (crs != null) {
-                final CoordinateSystem cs = crs.getCoordinateSystem();
-                for (int i=0; i<dimension; i++) {
-                    final DimensionNameType type = AXIS_DIRECTIONS.get(AxisDirections.absolute(cs.getAxis(i).getDirection()));
-                    if (type != null) {
-                        if (axisTypes == null) {
-                            axisTypes = new DimensionNameType[dimension];
-                        }
-                        axisTypes[i] = type;
-                    }
-                }
-            }
-            types = validateAxisTypes(axisTypes);
-        }
+    }
+
+    /**
+     * Creates a new grid extent with the same axes than the given extent, but different coordinates.
+     * This constructor does not invoke {@link #validateCoordinates()}; we presume that the caller's
+     * computation is correct.
+     *
+     * @param enclosing    the extent from which to copy axes, or {@code null} if none.
+     * @param coordinates  the coordinates. This array is not cloned.
+     */
+    GridExtent(final GridExtent enclosing, final long[] coordinates) {
+        this.coordinates = coordinates;
+        types = (enclosing != null) ? enclosing.types : null;
+        assert (types == null) || types.length == getDimension();
     }
 
     /**
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 9caedb6..976e9ff 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
@@ -106,7 +106,7 @@ import org.apache.sis.xml.NilReason;
  * The same instance can be shared by different {@link GridCoverage} instances.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -843,7 +843,7 @@ public class GridGeometry implements Serializable {
      * Returns the {@link #geographicBBox} value or {@code null} if none.
      * This method computes the box when first needed.
      */
-    private GeographicBoundingBox geographicBBox() {
+    final GeographicBoundingBox geographicBBox() {
         GeographicBoundingBox bbox = geographicBBox;
         if (bbox == null) {
             if (getCoordinateReferenceSystem(envelope) != null && !envelope.isAllNaN()) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/PointToGridCoordinates.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/PointToGridCoordinates.java
new file mode 100644
index 0000000..85dc4bc
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/PointToGridCoordinates.java
@@ -0,0 +1,81 @@
+/*
+ * 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 org.opengis.util.FactoryException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.CRS;
+
+
+/**
+ * Holds the object necessary for converting a geospatial coordinates to grid coordinates.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class PointToGridCoordinates {
+    /**
+     * The source coordinate reference system of the converter,
+     * or {@code null} if assumed the same than the coverage CRS.
+     */
+    final CoordinateReferenceSystem sourceCRS;
+
+    /**
+     * The transform from {@link #sourceCRS} to grid coordinates.
+     */
+    final MathTransform crsToGrid;
+
+    /**
+     * Creates a new objects holding the objects for converting from the given source CRS.
+     *
+     * <div class="note"><b>Note about coverage argument:</b>
+     * the {@code coverage} argument is for fetching the coverage CRS when needed.
+     * We could get that CRS from {@link GridCoverage#getCoordinateReferenceSystem()},
+     * but we use {@link GridCoverage#getCoordinateReferenceSystem()} instead for giving users a
+     * chance to override. We do not give the coverage CRS in argument because we want to invoke
+     * {@link GridCoverage#getCoordinateReferenceSystem()} only if {@code sourceCRS} is non-null,
+     * because {@code getCoordinateReferenceSystem()} may throw {@link IncompleteGridGeometryException}.
+     * </div>
+     *
+     * @param  sourceCRS      the source CRS, or {@code null} if assumed the same than the coverage CRS.
+     * @param  coverage       the coverage for which we are building those information.
+     * @param  gridGeometry   the coverage grid geometry.
+     * @throws TransformException if the {@link #crsToGrid} transform can not be built.
+     */
+    PointToGridCoordinates(final CoordinateReferenceSystem sourceCRS, final GridCoverage coverage,
+                           final GridGeometry gridGeometry) throws TransformException
+    {
+        this.sourceCRS = sourceCRS;
+        MathTransform tr = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER).inverse();
+        if (sourceCRS != null) try {
+            CoordinateOperation op = CRS.findOperation(sourceCRS,
+                    coverage.getCoordinateReferenceSystem(),        // See comment in above javadoc.
+                    gridGeometry.geographicBBox());
+            tr = MathTransforms.concatenate(op.getMathTransform(), tr);
+        } catch (FactoryException e) {
+            throw new TransformException(e.getMessage(), e);
+        }
+        crsToGrid = tr;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java
index 57e9a6f..f0bcf3b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java
@@ -16,91 +16,145 @@
  */
 package org.apache.sis.internal.coverage;
 
+import java.util.Collection;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
-import java.util.Collection;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.DirectPosition;
+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.opengis.coverage.CannotEvaluateException;
+import org.opengis.coverage.PointOutsideCoverageException;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.FractionalGridCoordinates;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
 import org.apache.sis.internal.image.TranslatedRenderedImage;
 import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.util.ArgumentChecks;
-import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.geometry.DirectPosition;
-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.opengis.util.FactoryException;
+
 
 /**
- * A {@link GridCoverage} with data stored in a {@link RenderedImage}.
+ * Basic access to grid data values backed by a two-dimensional {@link RenderedImage}.
+ * Each band in an image is represented as a {@link SampleDimension}.
+ * The rendered image can be a two-dimensional slice in a <var>n</var>-dimensional space
+ * (i.e. the {@linkplain GridGeometry#getEnvelope() grid geometry envelope} may have more
+ * than two dimensions) provided that the {@linkplain GridExtent grid extent} have a
+ * {@linkplain GridExtent#getSize size} equals to 1 in all dimensions except 2.
  *
- * @author Martin Desruisseaux (Geomatys)
- * @author Johann Sorel (Geomatys)
- * @version 2.0
- * @since   2.0
+ * <div class="note"><b>Example:</b>
+ * a remote sensing image may be valid only over some time range
+ * (the time of satellite pass over the observed area).
+ * Envelopes for such grid coverage can have three dimensions:
+ * the two usual ones (horizontal extent along <var>x</var> and <var>y</var>),
+ * and a third one for start time and end time (time extent along <var>t</var>).
+ * The "two-dimensional" grid coverage can have any number of columns along <var>x</var> axis
+ * and any number of rows along <var>y</var> axis, but only one plan along <var>t</var> axis.
+ * This single plan can have a lower bound (the start time) and an upper bound (the end time).
+ * </div>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.1
+ * @since   1.1
  * @module
  */
 public final class GridCoverage2D extends GridCoverage {
     /**
-     * The sample values, stored as a RenderedImage.
+     * The sample values stored as a {@code RenderedImage}.
      */
-    private final RenderedImage image;
-    private final int[] imageAxes;
-    private final CoordinateReferenceSystem crs2d;
+    private final RenderedImage data;
+
+    /**
+     * Index of extent dimensions corresponding to image <var>x</var> and <var>y</var> coordinates.
+     * Typical values are 0 for {@code xDimension} and 1 for {@code yDimension}, but different values
+     * are allowed.
+     */
+    private final int xDimension, yDimension;
+
+    /**
+     * The two-dimensional component of the coordinate reference system, or {@code null} if unspecified.
+     */
+    private final CoordinateReferenceSystem crs2D;
 
     /**
      * Result of the call to {@link #forConvertedValues(boolean)}, created when first needed.
      */
-    private GridCoverage converted;
+    private transient GridCoverage converted;
 
     /**
+     * Constructs a grid coverage using the specified domain, range and data.
      * The given RenderedImage may not start at 0,0, so does the gridExtent of the grid geometry.
      * Image 0/0 coordinate is expected to match grid extent lower corner.
      *
-     * @param grid  the grid extent, CRS and conversion from cell indices to CRS.
-     * @param bands sample dimensions for each image band.
-     * @param image the sample values as a RenderedImage, potentially multi-banded in packed view.
+     * @param  domain  the grid extent, CRS and conversion from cell indices to CRS.
+     * @param  range   sample dimensions for each image band.
+     * @param  data    the sample values as a RenderedImage, potentially multi-banded in packed view.
      */
-    public GridCoverage2D(final GridGeometry grid, final Collection<? extends SampleDimension> bands, final RenderedImage image) throws FactoryException {
-        super(grid, bands);
-        this.image = image;
-        ArgumentChecks.ensureNonNull("image", image);
-
-        //extract the 2D Coordinater
-        GridExtent extent = grid.getExtent();
-        imageAxes = extent.getSubspaceDimensions(2);
-        crs2d = CRS.reduce(grid.getCoordinateReferenceSystem(), imageAxes);
-
-        //check image is coherent with grid geometry
-        if (image.getWidth() != extent.getSize(imageAxes[0])) {
-            throw new IllegalArgumentException("Image width " + image.getWidth() + " does not match grid extent width "+ extent.getSize(imageAxes[0]));
+    public GridCoverage2D(final GridGeometry domain, final Collection<? extends SampleDimension> range, final RenderedImage data) {
+        super(domain, range);
+        this.data = data;
+        ArgumentChecks.ensureNonNull("image", data);
+        /*
+         * Extract the 2D components of the coordinate reference system.
+         */
+        final GridExtent extent = domain.getExtent();
+        final int[] imageAxes = extent.getSubspaceDimensions(2);
+        xDimension = imageAxes[0];
+        yDimension = imageAxes[1];
+        if (domain.isDefined(GridGeometry.CRS)) {
+            final CoordinateReferenceSystem crs = domain.getCoordinateReferenceSystem();
+            try {
+                crs2D = CRS.reduce(crs, imageAxes);
+            } catch (IllegalArgumentException | FactoryException e) {
+                throw new IllegalGridGeometryException("Can not create a two-dimensional CRS from " + crs.getName(), e);
+            }
+        } else {
+            crs2D = null;
         }
-        if (image.getHeight() != extent.getSize(imageAxes[1])) {
-            throw new IllegalArgumentException("Image height " + image.getHeight()+ " does not match grid extent height "+ extent.getSize(imageAxes[1]));
+        /*
+         * Check that image is coherent with grid geometry.
+         */
+        int  actual;
+        long expected;
+        if ((actual = data.getWidth()) != (expected = extent.getSize(xDimension))) {
+            throw new IllegalArgumentException("Image width " + actual + " does not match grid extent width " + expected);
         }
-        if (image.getSampleModel().getNumBands() != bands.size()) {
-            throw new IllegalArgumentException("Image sample model number of bands " + image.getSampleModel().getNumBands()+ " does not match number of sample dimensions "+ bands.size());
+        if ((actual = data.getHeight()) != (expected = extent.getSize(yDimension))) {
+            throw new IllegalArgumentException("Image height " + actual + " does not match grid extent height " + expected);
+        }
+        int n;
+        if ((actual = data.getSampleModel().getNumBands()) != (n = range.size())) {
+            throw new IllegalArgumentException("Image sample model number of bands " + actual + " does not match number of sample dimensions " + n);
         }
     }
 
     /**
-     * Returns the two-dimensional part of this grid coverage CRS. If the
-     * {@linkplain #getCoordinateReferenceSystem complete CRS} is two-dimensional, then this
-     * method returns the same CRS. Otherwise it returns a CRS for the two first axis having
-     * a {@linkplain GridExtent#getSize span} greater than 1 in the grid envelope. Note that
-     * those axis are guaranteed to appears in the same order than in the complete CRS.
+     * Returns the two-dimensional part of this grid coverage CRS.
+     * If the {@linkplain #getCoordinateReferenceSystem complete CRS} is two-dimensional,
+     * then this method returns the same CRS. Otherwise it returns a CRS for the two first axis
+     * having a {@linkplain GridExtent#getSize(int) size} greater than 1 in the grid envelope.
+     * Note that those axis are guaranteed to appear in the same order than in the complete CRS.
      *
-     * @return The two-dimensional part of the grid coverage CRS.
+     * @return the two-dimensional part of the grid coverage CRS.
+     * @throws IncompleteGridGeometryException if the grid geometry does not contain a CRS.
      *
-     * @see #getCoordinateReferenceSystem
+     * @see #getCoordinateReferenceSystem()
      */
     public CoordinateReferenceSystem getCoordinateReferenceSystem2D() {
-        return crs2d;
+        if (crs2D != null) {
+            return crs2D;
+        }
+        throw new IncompleteGridGeometryException(Resources.format(Resources.Keys.UnspecifiedCRS));
     }
 
     /**
@@ -111,7 +165,7 @@ public final class GridCoverage2D extends GridCoverage {
      */
     public MathTransform getGridToCrs2D() throws FactoryException {
         TransformSeparator sep = new TransformSeparator(getGridGeometry().getGridToCRS(PixelInCell.CELL_CENTER));
-        int idx = AxisDirections.indexOfColinear(getCoordinateReferenceSystem().getCoordinateSystem(), crs2d.getCoordinateSystem());
+        int idx = AxisDirections.indexOfColinear(getCoordinateReferenceSystem().getCoordinateSystem(), crs2D.getCoordinateSystem());
         sep.addSourceDimensionRange(idx, idx+2);
         return sep.separate();
     }
@@ -147,66 +201,58 @@ public final class GridCoverage2D extends GridCoverage {
      * @return the grid slice as a rendered image.
      */
     @Override
-    public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException {
+    public RenderedImage render(final GridExtent sliceExtent) throws CannotEvaluateException {
         if (sliceExtent == null || sliceExtent.equals(getGridGeometry().getExtent())) {
-            return image;
+            return data;
         } else {
-            final int subX = Math.toIntExact(sliceExtent.getLow(imageAxes[0]));
-            final int subY = Math.toIntExact(sliceExtent.getLow(imageAxes[1]));
-            final int subWidth = Math.toIntExact(Math.round(sliceExtent.getSize(imageAxes[0])));
-            final int subHeight = Math.toIntExact(Math.round(sliceExtent.getSize(imageAxes[1])));
+            final int subX = Math.toIntExact(sliceExtent.getLow(xDimension));
+            final int subY = Math.toIntExact(sliceExtent.getLow(yDimension));
+            final int subWidth = Math.toIntExact(Math.round(sliceExtent.getSize(xDimension)));
+            final int subHeight = Math.toIntExact(Math.round(sliceExtent.getSize(yDimension)));
 
-            if (image instanceof BufferedImage) {
-                final BufferedImage bi = (BufferedImage) image;
+            if (data instanceof BufferedImage) {
+                final BufferedImage bi = (BufferedImage) data;
                 return bi.getSubimage(subX, subY, subWidth, subHeight);
             } else {
-                return new TranslatedRenderedImage(image, subX, subY);
+                return new TranslatedRenderedImage(data, subX, subY);
             }
         }
     }
 
     /**
-     * {@inheritDoc }
+     * Returns a sequence of double values for a given point in the coverage.
+     * The CRS of the given point may be any coordinate reference system,
+     * or {@code null} for the same CRS than this coverage.
+     * The returned sequence contains a value for each {@linkplain SampleDimension sample dimension}.
+     *
+     * @param  point   the coordinate point where to evaluate.
+     * @param  buffer  an array in which to store values, or {@code null} to create a new array.
+     * @return the {@code buffer} array, or a newly created array if {@code buffer} was null.
+     * @throws PointOutsideCoverageException if the evaluation failed because the input point
+     *         has invalid coordinates.
+     * @throws CannotEvaluateException if the values can not be computed at the specified coordinate
+     *         for an other reason.
      */
     @Override
-    public double[] evaluate(DirectPosition position, double[] buffer) throws CannotEvaluateException {
+    public double[] evaluate(final DirectPosition point, double[] buffer) throws CannotEvaluateException {
         try {
-            position = toGridCoord(position);
-            long[] coord = toLongExact(position);
-            int x = Math.toIntExact(Math.round(coord[imageAxes[0]]));
-            int y = Math.toIntExact(Math.round(coord[imageAxes[1]]));
-            return image.getTile(XToTileX(x), YToTileY(y)).getPixel(x, y, buffer);
-        } catch (FactoryException | TransformException ex) {
+            final FractionalGridCoordinates gc = toGridCoordinates(point);
+            final int x = Math.toIntExact(gc.getCoordinateValue(xDimension));
+            final int y = Math.toIntExact(gc.getCoordinateValue(yDimension));
+            final int xmin = data.getMinX();
+            final int ymin = data.getMinY();
+            if (x >= xmin && x < xmin + (long) data.getWidth() &&
+                y >= ymin && y < ymin + (long) data.getHeight())
+            {
+                final int tx = Math.floorDiv(x - data.getTileGridXOffset(), data.getTileWidth());
+                final int ty = Math.floorDiv(y - data.getTileGridYOffset(), data.getTileHeight());
+                return data.getTile(tx, ty).getPixel(x, y, buffer);
+            }
+        } catch (ArithmeticException | DisjointExtentException ex) {
+            throw (PointOutsideCoverageException) new PointOutsideCoverageException(ex.getMessage(), point).initCause(ex);
+        } catch (IllegalArgumentException | TransformException ex) {
             throw new CannotEvaluateException(ex.getMessage(), ex);
         }
+        throw new PointOutsideCoverageException(null, point);
     }
-
-    /**
-     * Converts a pixel's X coordinate into a horizontal tile index.
-     * @param x pixel x coordinate
-     * @return tile x coordinate
-     */
-    private int XToTileX(int x) {
-        int tileWidth = image.getTileWidth();
-        x -= image.getTileGridXOffset();
-        if (x < 0) {
-            x += 1 - tileWidth;
-        }
-        return x/tileWidth;
-    }
-
-    /**
-     * Converts a pixel's Y coordinate into a vertical tile index.
-     * @param y pixel x coordinate
-     * @return tile y coordinate
-     */
-    private int YToTileY(int y) {
-        int tileHeight = image.getTileHeight();
-        y -= image.getTileGridYOffset();
-        if (y < 0) {
-            y += 1 - tileHeight;
-        }
-        return y/tileHeight;
-    }
-
 }
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 76ef8c3..3e23ce6 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
@@ -299,6 +299,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short UnavailableGeometryLibrary_1 = 21;
 
         /**
+         * Can not convert grid coordinate {1} to type ‘{0}’.
+         */
+        public static final short UnconvertibleGridCoordinate_2 = 59;
+
+        /**
          * Expected {0} bands but got {1}.
          */
         public static final short UnexpectedNumberOfBands_2 = 49;
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 8ae3d8a..f1153af 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
@@ -65,6 +65,7 @@ PropertyAlreadyExists_2           = Property \u201c{1}\u201d already exists in f
 PropertyNotFound_2                = No property named \u201c{1}\u201d has been found in \u201c{0}\u201d feature.
 TooManyQualitatives               = Too many qualitative categories.
 UnavailableGeometryLibrary_1      = The {0} geometry library is not available in current runtime environment.
+UnconvertibleGridCoordinate_2     = Can not convert grid coordinate {1} to type \u2018{0}\u2019.
 UnexpectedNumberOfBands_2         = Expected {0} bands but got {1}.
 UnexpectedNumberOfComponents_4    = The \u201c{1}\u201d value given to \u201c{0}\u201d property should be separable in {2} components, but we got {3}.
 UnexpectedNumberOfCoordinates_4   = The \u201c{0}\u201d feature at {1} has a {3} coordinate values, while we expected a multiple of {2}.
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 3828cfa..a0f13a6 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
@@ -71,6 +71,7 @@ PropertyAlreadyExists_2           = La propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f
 PropertyNotFound_2                = Aucune propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb n\u2019a \u00e9t\u00e9 trouv\u00e9e dans l\u2019entit\u00e9 \u00ab\u202f{0}\u202f\u00bb.
 TooManyQualitatives               = Trop de cat\u00e9gories qualitatives.
 UnavailableGeometryLibrary_1      = La biblioth\u00e8que de g\u00e9om\u00e9tries {0} n\u2019est pas disponible dans l\u2019environnement d\u2019ex\u00e9cution actuel.
+UnconvertibleGridCoordinate_2     = Ne peut pas convertir la coordonn\u00e9e de grille {1} vers le type \u2018{0}\u2019.
 UnexpectedNumberOfBands_2         = On attendait {0} bandes mais {1} ont \u00e9t\u00e9 sp\u00e9cifi\u00e9es.
 UnexpectedNumberOfComponents_4    = La valeur \u00ab\u202f{1}\u202f\u00bb donn\u00e9e \u00e0 la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb devrait \u00eatre s\u00e9parable en {2} composantes, mais on en a obtenus {3}.
 UnexpectedNumberOfCoordinates_4   = L\u2019entit\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb \u00e0 {1} contient {3} coordonn\u00e9es, alors qu\u2019on attendait un multiple de {2}.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java
new file mode 100644
index 0000000..bb9e0b5
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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 org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests the {@link FractionalGridCoordinates} implementation.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class FractionalGridCoordinatesTest extends TestCase {
+    /**
+     * Creates a test instance with (4 -1.1 7.6) coordinate values.
+     */
+    private static FractionalGridCoordinates instance() {
+        final FractionalGridCoordinates gc = new FractionalGridCoordinates(3);
+        gc.coordinates[0] =  4;
+        gc.coordinates[1] = -1.1;
+        gc.coordinates[2] =  7.6;
+        return gc;
+    }
+
+    /**
+     * Tests {@link FractionalGridCoordinates#getCoordinateValue(int)}.
+     */
+    @Test
+    public void testGetCoordinateValue() {
+        final FractionalGridCoordinates gc = instance();
+        assertEquals( 4, gc.getCoordinateValue(0));
+        assertEquals(-1, gc.getCoordinateValue(1));
+        assertEquals( 8, gc.getCoordinateValue(2));
+    }
+
+    /**
+     * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)}
+     * with default parameter values.
+     */
+    @Test
+    public void testToExtent() {
+        final GridExtent extent = instance().toExtent(null);
+        GridExtentTest.assertExtentEquals(extent, 0,  4,  4);
+        GridExtentTest.assertExtentEquals(extent, 1, -2, -1);
+        GridExtentTest.assertExtentEquals(extent, 2,  7,  8);
+    }
+
+    /**
+     * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a size of 1.
+     */
+    @Test
+    public void testToExtentSize1() {
+        final GridExtent extent = instance().toExtent(null, 1, 1, 1);
+        GridExtentTest.assertExtentEquals(extent, 0,  4,  4);
+        GridExtentTest.assertExtentEquals(extent, 1, -1, -1);
+        GridExtentTest.assertExtentEquals(extent, 2,  8,  8);
+    }
+
+    /**
+     * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a size greater than 2.
+     */
+    @Test
+    public void testToExtentSizeN() {
+        final GridExtent extent = instance().toExtent(null, 3, 5, 4);
+        GridExtentTest.assertExtentEquals(extent, 0,  3,  5);
+        GridExtentTest.assertExtentEquals(extent, 1, -3,  1);
+        GridExtentTest.assertExtentEquals(extent, 2,  6,  9);
+    }
+
+    /**
+     * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a bounds constraint.
+     */
+    @Test
+    public void testToExtentBounded() {
+        final GridExtent bounds = new GridExtent(null, null, new long[] {4, 2, 7}, true);
+        final GridExtent extent = instance().toExtent(bounds, 3, 5, 4);
+        GridExtentTest.assertExtentEquals(extent, 0,  2,  4);
+        GridExtentTest.assertExtentEquals(extent, 1,  0,  2);
+        GridExtentTest.assertExtentEquals(extent, 2,  4,  7);
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java
index d151ed0..0904f66 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java
@@ -102,8 +102,7 @@ public class BufferedGridCoverageTest extends TestCase {
             { -60, -195},
             {-216, -380}
         });
-
-        /**
+        /*
          * Test evaluation
          */
         Assert.assertArrayEquals(new double[]{ 70.0}, coverage.evaluate(new DirectPosition2D(0, 0), null), STRICT);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java
index 073a100..0bc953b 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java
@@ -112,8 +112,7 @@ public class GridCoverage2DTest extends TestCase {
             { -60, -195},
             {-216, -380}
         });
-
-        /**
+        /*
          * Test evaluation
          */
         Assert.assertArrayEquals(new double[]{ 70.0}, coverage.evaluate(new DirectPosition2D(0, 0), null), STRICT);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 3058a84..2a59156 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -80,6 +80,7 @@ import org.junit.runners.Suite;
     org.apache.sis.coverage.grid.GridExtentTest.class,
     org.apache.sis.coverage.grid.GridGeometryTest.class,
     org.apache.sis.coverage.grid.GridDerivationTest.class,
+    org.apache.sis.coverage.grid.FractionalGridCoordinates.class,
     org.apache.sis.coverage.CategoryTest.class,
     org.apache.sis.coverage.CategoryListTest.class,
     org.apache.sis.coverage.SampleDimensionTest.class,


Mime
View raw message