sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 04/04: Remove GridCoverage.evaluate(…) method, replaced by an `Evaluator` class. This commit is only a refactoring without changes in functionalities. Future commits will add more capabilities or optimizations.
Date Tue, 09 Jun 2020 17:38:08 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 2c3e3bef120de086b581bc1e5f4b6259e55d903a
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Jun 9 17:48:47 2020 +0200

    Remove GridCoverage.evaluate(…) method, replaced by an `Evaluator` class.
    This commit is only a refactoring without changes in functionalities.
    Future commits will add more capabilities or optimizations.
    
    https://issues.apache.org/jira/browse/SIS-485
---
 .../org/apache/sis/gui/map/ValuesUnderCursor.java  |  32 ++-
 .../sis/coverage/grid/ConvertedGridCoverage.java   |  61 ++++--
 .../org/apache/sis/coverage/grid/Evaluator.java    | 234 +++++++++++++++++++++
 .../coverage/grid/FractionalGridCoordinates.java   |  33 +--
 .../org/apache/sis/coverage/grid/GridCoverage.java | 123 ++---------
 .../apache/sis/coverage/grid/GridCoverage2D.java   |  67 +++---
 .../sis/coverage/grid/PointToGridCoordinates.java  |  81 -------
 .../sis/coverage/grid/ResampledGridCoverage.java   |  10 +-
 .../sis/coverage/grid/GridCoverage2DTest.java      |  22 +-
 9 files changed, 364 insertions(+), 299 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
index 8b8aae5..a191e6c 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
@@ -43,6 +43,7 @@ import org.opengis.metadata.content.TransferFunctionType;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.gui.coverage.CoverageCanvas;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.Evaluator;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.internal.system.Modules;
@@ -214,11 +215,9 @@ public abstract class ValuesUnderCursor {
         private static final int SCIENTIFIC_NOTATION = -2;
 
         /**
-         * The source of values converted to the {@linkplain #units} of measurement.
-         * Not necessarily the same instance than the property this {@code FromCoverage} is listening,
-         * since we take the instance returned by {@link GridCoverage#forConvertedValues(boolean)}.
+         * The object computing or interpolation sample values in the coverage.
          */
-        private GridCoverage coverage;
+        private Evaluator evaluator;
 
         /**
          * The selection status of each band.
@@ -226,12 +225,7 @@ public abstract class ValuesUnderCursor {
         private final BitSet selectedBands;
 
         /**
-         * A temporary buffer for getting numerical values. Created when first needed.
-         */
-        private double[] results;
-
-        /**
-         * Formatter for {@link #results} values.
+         * Formatter for the values computed or interpolated by {@link #evaluator}.
          * The number of fraction digits is computed from transfer function resolution.
          * The same {@link NumberFormat} instance may appear at more than one index.
          */
@@ -297,11 +291,11 @@ public abstract class ValuesUnderCursor {
          */
         @Override
         public void changed(final ObservableValue<? extends GridCoverage> property,
-                            final GridCoverage previous, GridCoverage coverage)
+                            final GridCoverage previous, final GridCoverage coverage)
         {
             final List<SampleDimension> bands;      // Should never be null, but check anyway.
             if (coverage == null || (bands = coverage.getSampleDimensions()) == null) {
-                this.coverage = null;
+                evaluator     = null;
                 units         = null;
                 sampleFormats = null;
                 outsideText   = null;
@@ -310,8 +304,8 @@ public abstract class ValuesUnderCursor {
                 valueChoices.getItems().clear();
                 return;
             }
-            this.coverage = coverage.forConvertedValues(true);
-            if (previous != null && bands.equals(previous.forConvertedValues(true).getSampleDimensions())) {
+            evaluator = coverage.forConvertedValues(true).evaluator();
+            if (previous != null && bands.equals(previous.getSampleDimensions())) {
                 // Same configuration than previous coverage.
                 return;
             }
@@ -466,11 +460,11 @@ public abstract class ValuesUnderCursor {
          * @param  point  the cursor location in arbitrary CRS, or {@code null} if outside canvas region.
          * @return string representation of data under given position, or {@code null} if none.
          *
-         * @see GridCoverage#evaluate(DirectPosition, double[])
+         * @see Evaluator#apply(DirectPosition)
          */
         @Override
         public String evaluate(final DirectPosition point) {
-            if (outsideText == null) {
+            if (outsideText == null && evaluator != null) {
                 onBandSelectionChanged();
             }
             if (point != null) {
@@ -482,8 +476,8 @@ public abstract class ValuesUnderCursor {
                  */
                 synchronized (buffer) {
                     buffer.setLength(0);
-                    if (coverage != null) try {
-                        results = coverage.evaluate(point, results);
+                    if (evaluator != null) try {
+                        final double[] results = evaluator.apply(point);
                         if (results != null) {
                             for (int i = -1; (i = selectedBands.nextSetBit(i+1)) >= 0;) {
                                 if (buffer.length() != 0) {
@@ -551,7 +545,7 @@ public abstract class ValuesUnderCursor {
          */
         private boolean onBandSelectionChanged() {
             final ObservableList<MenuItem> menus = valueChoices.getItems();
-            final List<SampleDimension>    bands = coverage.getSampleDimensions();
+            final List<SampleDimension>    bands = evaluator.getCoverage().getSampleDimensions();
             final StringBuilder            names = new StringBuilder().append('(');
             final String text;
             synchronized (buffer) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
index f039471..787e6e0 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
@@ -44,7 +44,7 @@ import org.apache.sis.measure.NumberRange;
  *   <li>In calls to {@link #render(GridExtent)}, sample values are converted when first needed
  *       on a tile-by-tile basis then cached for future reuse. Note however that discarding the
  *       returned image may result in the lost of cached tiles.</li>
- *   <li>In calls to {@link #evaluate(DirectPosition, double[])}, the conversion is applied
+ *   <li>In calls to {@link Evaluator#apply(DirectPosition)}, the conversion is applied
  *       on-the-fly each time in order to avoid the potentially costly tile computations.</li>
  * </ul>
  *
@@ -176,25 +176,56 @@ final class ConvertedGridCoverage extends GridCoverage {
     }
 
     /**
-     * Returns a sequence of double values for a given point in the coverage.
-     * This method delegates to the source coverage, then converts the values.
+     * Creates a new function for computing or interpolating sample values at given locations.
      *
-     * @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 CannotEvaluateException if the values can not be computed.
+     * <h4>Multi-threading</h4>
+     * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
+     * a new {@link Evaluator} instance should be created for each thread.
+     *
+     * @since 1.1
      */
     @Override
-    public double[] evaluate(final DirectPosition point, double[] buffer) throws CannotEvaluateException {
-        try {
-            buffer = source.evaluate(point, buffer);
-            for (int i=0; i<converters.length; i++) {
-                buffer[i] = converters[i].transform(buffer[i]);
+    public Evaluator evaluator() {
+        return new SampleConverter();
+    }
+
+    /**
+     * Implementation of evaluator returned by {@link #evaluator()}.
+     */
+    private final class SampleConverter extends Evaluator {
+        /**
+         * The evaluator provided by source coverage.
+         */
+        private final Evaluator evaluator;
+
+        /**
+         * Creates a new evaluator for the enclosing coverage.
+         */
+        SampleConverter() {
+            super(ConvertedGridCoverage.this);
+            evaluator = source.evaluator();
+        }
+
+        /**
+         * Returns a sequence of double values for a given point in the coverage.
+         * This method delegates to the source coverage, then converts the values.
+         *
+         * @param  point  the coordinate point where to evaluate.
+         * @throws CannotEvaluateException if the values can not be computed.
+         */
+        @Override
+        public double[] apply(final DirectPosition point) throws CannotEvaluateException {
+            final double[] values;
+            try {
+                values = evaluator.apply(point);
+                for (int i=0; i<converters.length; i++) {
+                    values[i] = converters[i].transform(values[i]);
+                }
+            } catch (TransformException ex) {
+                throw new CannotEvaluateException(ex.getMessage(), ex);
             }
-        } catch (TransformException ex) {
-            throw new CannotEvaluateException(ex.getMessage(), ex);
+            return values;
         }
-        return buffer;
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/Evaluator.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/Evaluator.java
new file mode 100644
index 0000000..b7c16e2
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/Evaluator.java
@@ -0,0 +1,234 @@
+/*
+ * 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.function.Function;
+import java.awt.image.RenderedImage;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.coverage.CannotEvaluateException;
+import org.opengis.coverage.PointOutsideCoverageException;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * Computes or interpolates values of sample dimensions at given positions.
+ *
+ * <h2>Multi-threading</h2>
+ * Evaluators are not thread-safe. An instance of {@code Evaluator} should be created
+ * for each thread that need to compute sample values.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @see GridCoverage#evaluator()
+ *
+ * @since 1.1
+ * @module
+ */
+public class Evaluator implements Function<DirectPosition, double[]> {
+    /**
+     * The coverage in which to evaluate sample values.
+     */
+    protected final GridCoverage coverage;
+
+    /**
+     * The source coordinate reference system of the converter,
+     * or {@code null} if assumed the same than the coverage CRS.
+     */
+    private CoordinateReferenceSystem sourceCRS;
+
+    /**
+     * The transform from {@link #sourceCRS} to grid coordinates.
+     * 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 MathTransform crsToGrid;
+
+    /**
+     * Grid coordinates after {@link #crsToGrid} conversion.
+     *
+     * @see #toGridPosition(DirectPosition)
+     */
+    private FractionalGridCoordinates.Position position;
+
+    /**
+     * Array where to store sample values computed by {@link #apply(DirectPosition)}.
+     * For performance reasons, the same array may be recycled on every method call.
+     */
+    private double[] values;
+
+    /**
+     * Creates a new evaluator for the given coverage. This constructor is protected for allowing
+     * {@link GridCoverage} subclasses to provide their own {@code Evaluator} implementations.
+     * For using an evaluator, invoke {@link GridCoverage#evaluator()} instead.
+     *
+     * @param  coverage  the coverage for which to create an evaluator.
+     *
+     * @see GridCoverage#evaluator()
+     */
+    protected Evaluator(final GridCoverage coverage) {
+        ArgumentChecks.ensureNonNull("coverage", coverage);
+        this.coverage = coverage;
+    }
+
+    /**
+     * Returns the coverage from which this evaluator is fetching sample values.
+     *
+     * @return the source of sample values for this evaluator.
+     */
+    public GridCoverage getCoverage() {
+        return coverage;
+    }
+
+    /**
+     * 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 the {@linkplain #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 GridCoverage#render(GridExtent)} for a small region
+     * around the point. Subclasses should override with more efficient implementation.</p>
+     *
+     * @param  point   the coordinate point where to evaluate.
+     * @return the sample values at the specified point. For performance reason, this method may return
+     *         the same array on every method call by overwriting previous values.
+     *         Callers should not assume that the array content stay valid for a long time.
+     * @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 another reason. This exception 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.
+     */
+    @Override
+    public double[] apply(final DirectPosition point) 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 GridGeometry gridGeometry = coverage.gridGeometry;
+        final long[] size = new long[gridGeometry.getDimension()];
+        java.util.Arrays.fill(size, 1);
+        try {
+            final FractionalGridCoordinates gc = toGridPosition(point);
+            try {
+                final GridExtent subExtent = gc.toExtent(gridGeometry.extent, size);
+                return evaluate(coverage.render(subExtent), 0, 0);
+            } catch (ArithmeticException | IndexOutOfBoundsException | DisjointExtentException ex) {
+                throw (PointOutsideCoverageException) new PointOutsideCoverageException(
+                        gc.pointOutsideCoverage(gridGeometry.extent), point).initCause(ex);
+            }
+        } catch (PointOutsideCoverageException ex) {
+            ex.setOffendingLocation(point);
+            throw ex;
+        } catch (RuntimeException | TransformException ex) {
+            throw new CannotEvaluateException(ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * Gets sample values from the given image at the given index. This method does not verify
+     * explicitly if the coordinates are out of bounds; we rely on the checks performed by the
+     * image and sample model implementations.
+     *
+     * @param  data  the data from which to get the sample values.
+     * @param  x     column index of the value to get.
+     * @param  y     row index of the value to get.
+     * @return the sample values. The same array may be recycled on every method call.
+     * @throws ArithmeticException if an integer overflow occurred while computing indices.
+     * @throws IndexOutOfBoundsException if a coordinate is out of bounds.
+     */
+    final double[] evaluate(final RenderedImage data, final int x, final int y) {
+        final int tx = ImageUtilities.pixelToTileX(data, x);
+        final int ty = ImageUtilities.pixelToTileY(data, y);
+        return values = data.getTile(tx, ty).getPixel(x, y, values);
+    }
+
+    /**
+     * 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
+     * {@linkplain #coverage} CRS, then this method automatically transforms that position to the
+     * {@linkplain GridCoverage#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 GridCoverage#getGridGeometry() grid geometry}
+     *         does not define a "grid to CRS" transform, or if the given point has a non-null CRS but the
+     *         {@linkplain #coverage} does not {@linkplain GridCoverage#getCoordinateReferenceSystem() have a CRS}.
+     * @throws TransformException if the given coordinates can not be transformed.
+     *
+     * @see FractionalGridCoordinates#toPosition(MathTransform)
+     */
+    public FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException {
+        ArgumentChecks.ensureNonNull("point", point);
+        return new FractionalGridCoordinates(toGridPosition(point));
+    }
+
+    /**
+     * Updates the grid {@linkplain #position} with the given geospatial position.
+     * This is the implementation of {@link #toGridCoordinates(DirectPosition)} except
+     * that it avoid creating a new {@link FractionalGridCoordinates} on each method call.
+     *
+     * @param  point  the geospatial position.
+     * @return the given position converted to grid coordinates (possibly out of grid bounds).
+     * @throws TransformException if the given position can not be converted.
+     */
+    final FractionalGridCoordinates.Position toGridPosition(final DirectPosition point) throws TransformException {
+        final CoordinateReferenceSystem crs = point.getCoordinateReferenceSystem();
+        if (crs != sourceCRS || crsToGrid == null) {
+            final GridGeometry gridGeometry = coverage.getGridGeometry();
+            MathTransform tr = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER).inverse();
+            if (crs != null) try {
+                CoordinateOperation op = CRS.findOperation(crs,
+                        coverage.getCoordinateReferenceSystem(),
+                        gridGeometry.geographicBBox());
+                tr = MathTransforms.concatenate(op.getMathTransform(), tr);
+            } catch (FactoryException e) {
+                throw new TransformException(e.getMessage(), e);
+            }
+            position  = new FractionalGridCoordinates.Position(tr.getTargetDimensions());
+            crsToGrid = tr;
+            sourceCRS = crs;
+        }
+        final DirectPosition result = crsToGrid.transform(point, position);
+        if (result != position) {
+            final double[] coordinates = result.getCoordinate();
+            System.arraycopy(coordinates, 0, position.coordinates, 0, position.coordinates.length);
+        }
+        return position;
+    }
+}
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
index bab43ac..2591d30 100644
--- 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
@@ -50,7 +50,7 @@ import org.apache.sis.util.resources.Errors;
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  *
- * @see GridCoverage#toGridCoordinates(DirectPosition)
+ * @see Evaluator#toGridCoordinates(DirectPosition)
  *
  * @since 1.1
  * @module
@@ -71,7 +71,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
      *
      * <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)
+     * indirectly for example from the {@linkplain Evaluator#toGridCoordinates(DirectPosition)
      * conversion of a geospatial position}.</div>
      *
      * @param  dimension  the number of dimensions.
@@ -324,31 +324,6 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
     }
 
     /**
-     * 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}.
@@ -357,7 +332,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
      * @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)
+     * @see Evaluator#toGridCoordinates(DirectPosition)
      */
     public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
         return gridToCRS.transform(new Position(this), null);
@@ -371,7 +346,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
      * <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 {
+    static final class Position extends FractionalGridCoordinates implements DirectPosition {
         /**
          * For cross-version compatibility.
          */
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 7109f72..131036d 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,23 +19,18 @@ package org.apache.sis.coverage.grid;
 import java.util.List;
 import java.util.Collection;
 import java.util.Locale;
-import java.util.Objects;
 import java.util.Optional;
 import java.awt.image.ColorModel;
 import java.awt.image.RenderedImage;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
 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.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
-import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTable;
@@ -46,7 +41,6 @@ import org.apache.sis.util.Debug;
 
 // Branch-specific imports
 import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.coverage.PointOutsideCoverageException;
 
 
 /**
@@ -87,13 +81,6 @@ public abstract class GridCoverage {
     private transient GridCoverage packedView, convertedView;
 
     /**
-     * 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.
@@ -127,14 +114,12 @@ public abstract class GridCoverage {
 
     /**
      * Returns the coordinate reference system to which the values in grid domain are referenced.
-     * This is the CRS used when accessing a coverage with the {@code evaluate(…)} methods.
-     * This coordinate reference system is usually different than the coordinate system of the grid.
-     * It is the target coordinate reference system of the {@link GridGeometry#getGridToCRS gridToCRS}
+     * This is the target coordinate reference system of the {@link GridGeometry#getGridToCRS gridToCRS}
      * math transform.
      *
      * <p>The default implementation delegates to {@link GridGeometry#getCoordinateReferenceSystem()}.</p>
      *
-     * @return the CRS used when accessing a coverage with the {@code evaluate(…)} methods.
+     * @return the "real world" CRS of this coverage.
      * @throws IncompleteGridGeometryException if the grid geometry has no CRS.
      */
     public CoordinateReferenceSystem getCoordinateReferenceSystem() {
@@ -251,105 +236,21 @@ public abstract class GridCoverage {
     }
 
     /**
-     * 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 another 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.
+     * Creates a new function for computing or interpolating sample values at given locations.
+     * That function accepts {@link DirectPosition} in arbitrary Coordinate Reference System;
+     * conversion to grid indices are applied as needed.
      *
-     * @since 1.1
-     */
-    public double[] evaluate(final DirectPosition point, final 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 FractionalGridCoordinates gc = toGridCoordinates(point);
-            try {
-                final GridExtent subExtent = gc.toExtent(gridGeometry.extent, size);
-                return evaluate(render(subExtent), 0, 0, buffer);
-            } catch (ArithmeticException | IndexOutOfBoundsException | DisjointExtentException ex) {
-                throw (PointOutsideCoverageException) new PointOutsideCoverageException(
-                        gc.pointOutsideCoverage(gridGeometry.extent), point).initCause(ex);
-            }
-        } catch (PointOutsideCoverageException ex) {
-            ex.setOffendingLocation(point);
-            throw ex;
-        } catch (RuntimeException | TransformException ex) {
-            throw new CannotEvaluateException(ex.getMessage(), ex);
-        }
-    }
-
-    /**
-     * Gets sample values from the given image at the given index. This method does not verify
-     * explicitly if the coordinates are out of bounds; we rely on the checks performed by the
-     * image and sample model implementations.
-     *
-     * @param  data    the data from which to get the sample values.
-     * @param  x       column index of the value to get.
-     * @param  y       row index of the value to get.
-     * @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 ArithmeticException if an integer overflow occurred while computing indices.
-     * @throws IndexOutOfBoundsException if a coordinate is out of bounds.
-     */
-    static double[] evaluate(final RenderedImage data, final int x, final int y, final double[] buffer) {
-        final int tx = ImageUtilities.pixelToTileX(data, x);
-        final int ty = ImageUtilities.pixelToTileY(data, y);
-        return data.getTile(tx, ty).getPixel(x, y, buffer);
-    }
-
-    /**
-     * 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>
+     * <h4>Multi-threading</h4>
+     * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
+     * a new {@link Evaluator} instance should be created for each thread by invoking this
+     * method multiply times.
      *
-     * @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)
+     * @return a new function for computing or interpolating sample values.
      *
      * @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);
+    public Evaluator evaluator() {
+        return new Evaluator(this);
     }
 
     /**
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 373eec0..6f46031 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
@@ -496,36 +496,53 @@ public class GridCoverage2D extends GridCoverage {
     }
 
     /**
-     * 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}.
+     * Creates a new function for computing or interpolating sample values at given locations.
      *
-     * @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 another reason.
+     * <h4>Multi-threading</h4>
+     * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
+     * a new {@link Evaluator} instance should be created for each thread.
+     *
+     * @since 1.1
      */
     @Override
-    public double[] evaluate(final DirectPosition point, final double[] buffer) throws CannotEvaluateException {
-        try {
-            final FractionalGridCoordinates gc = toGridCoordinates(point);
+    public Evaluator evaluator() {
+        return new PixelAccessor();
+    }
+
+    /**
+     * Implementation of evaluator returned by {@link #evaluator()}.
+     */
+    private final class PixelAccessor extends Evaluator {
+        /**
+         * Creates a new evaluator for the enclosing coverage.
+         */
+        PixelAccessor() {
+            super(GridCoverage2D.this);
+        }
+
+        /**
+         * 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 the coverage.
+         */
+        @Override
+        public double[] apply(final DirectPosition point) throws CannotEvaluateException {
             try {
-                final int x = toIntExact(addExact(gc.getCoordinateValue(xDimension), gridToImageX));
-                final int y = toIntExact(addExact(gc.getCoordinateValue(yDimension), gridToImageY));
-                return evaluate(data, x, y, buffer);
-            } catch (ArithmeticException | IndexOutOfBoundsException | DisjointExtentException ex) {
-                throw (PointOutsideCoverageException) new PointOutsideCoverageException(
-                        gc.pointOutsideCoverage(gridGeometry.extent), point).initCause(ex);
+                final FractionalGridCoordinates gc = toGridPosition(point);
+                try {
+                    final int x = toIntExact(addExact(gc.getCoordinateValue(xDimension), gridToImageX));
+                    final int y = toIntExact(addExact(gc.getCoordinateValue(yDimension), gridToImageY));
+                    return evaluate(data, x, y);
+                } catch (ArithmeticException | IndexOutOfBoundsException | DisjointExtentException ex) {
+                    throw (PointOutsideCoverageException) new PointOutsideCoverageException(
+                            gc.pointOutsideCoverage(gridGeometry.extent), point).initCause(ex);
+                }
+            } catch (PointOutsideCoverageException ex) {
+                ex.setOffendingLocation(point);
+                throw ex;
+            } catch (RuntimeException | TransformException ex) {
+                throw new CannotEvaluateException(ex.getMessage(), ex);
             }
-        } catch (PointOutsideCoverageException ex) {
-            ex.setOffendingLocation(point);
-            throw ex;
-        } catch (RuntimeException | TransformException ex) {
-            throw new CannotEvaluateException(ex.getMessage(), ex);
         }
     }
 
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
deleted file mode 100644
index 85dc4bc..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/PointToGridCoordinates.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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/coverage/grid/ResampledGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
index 3ba691f..c1905df 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
@@ -22,7 +22,6 @@ import java.util.Optional;
 import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
 import org.opengis.util.FactoryException;
-import org.opengis.geometry.DirectPosition;
 import org.opengis.coverage.CannotEvaluateException;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -531,14 +530,9 @@ final class ResampledGridCoverage extends GridCoverage {
 
     /**
      * Delegates to the source coverage, which should transform the point itself if needed.
-     *
-     * @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 CannotEvaluateException if the values can not be computed at the specified coordinate.
      */
     @Override
-    public double[] evaluate(final DirectPosition point, final double[] buffer) throws CannotEvaluateException {
-        return source.evaluate(point, buffer);
+    public Evaluator evaluator() {
+        return source.evaluator();
     }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
index 91170a0..525df47 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
@@ -180,35 +180,35 @@ public strictfp class GridCoverage2DTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridCoverage2D#evaluate(DirectPosition, double[])}.
+     * Tests {@link Evaluator#apply(DirectPosition)}.
      */
     @Test
-    public void testEvaluate() {
-        final GridCoverage coverage = createTestCoverage();
+    public void testEvaluator() {
+        final Evaluator evaluator = createTestCoverage().evaluator();
         /*
          * Test evaluation at indeger indices. No interpolation should be applied.
          */
-        assertArrayEquals(new double[] {  2}, coverage.evaluate(new DirectPosition2D(0, 0), null), STRICT);
-        assertArrayEquals(new double[] {  5}, coverage.evaluate(new DirectPosition2D(1, 0), null), STRICT);
-        assertArrayEquals(new double[] { -5}, coverage.evaluate(new DirectPosition2D(0, 1), null), STRICT);
-        assertArrayEquals(new double[] {-10}, coverage.evaluate(new DirectPosition2D(1, 1), null), STRICT);
+        assertArrayEquals(new double[] {  2}, evaluator.apply(new DirectPosition2D(0, 0)), STRICT);
+        assertArrayEquals(new double[] {  5}, evaluator.apply(new DirectPosition2D(1, 0)), STRICT);
+        assertArrayEquals(new double[] { -5}, evaluator.apply(new DirectPosition2D(0, 1)), STRICT);
+        assertArrayEquals(new double[] {-10}, evaluator.apply(new DirectPosition2D(1, 1)), STRICT);
         /*
          * Test evaluation at fractional indices. Current interpolation is nearest neighor rounding,
          * but future version may do a bilinear interpolation.
          */
-        assertArrayEquals(new double[] {2}, coverage.evaluate(new DirectPosition2D(-0.499, -0.499), null), STRICT);
-        assertArrayEquals(new double[] {2}, coverage.evaluate(new DirectPosition2D( 0.499,  0.499), null), STRICT);
+        assertArrayEquals(new double[] {2}, evaluator.apply(new DirectPosition2D(-0.499, -0.499)), STRICT);
+        assertArrayEquals(new double[] {2}, evaluator.apply(new DirectPosition2D( 0.499,  0.499)), STRICT);
         /*
          * Test some points that are outside the coverage extent.
          */
         try {
-            coverage.evaluate(new DirectPosition2D(-0.51, 0), null);
+            evaluator.apply(new DirectPosition2D(-0.51, 0));
             fail("Expected PointOutsideCoverageException.");
         } catch (PointOutsideCoverageException ex) {
             assertNotNull(ex.getMessage());
         }
         try {
-            coverage.evaluate(new DirectPosition2D(1.51, 0), null);
+            evaluator.apply(new DirectPosition2D(1.51, 0));
             fail("Expected PointOutsideCoverageException.");
         } catch (PointOutsideCoverageException ex) {
             assertNotNull(ex.getMessage());


Mime
View raw message