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 52cff9a020dd7b8dd4f55e7520cebd76f9dceba7
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Nov 25 15:31:05 2021 +0100

    Move `RenderingData` to the portrayal module without the pseudo-styling 
part.
    It will allows the reuse of this rendering code for targets other than 
JavaFX.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  12 +-
 .../gui/coverage/MultiResolutionImageLoader.java   |   4 +-
 .../sis/gui/coverage/StyledRenderingData.java      | 104 ++++++++
 .../coverage/MultiResolutionCoverageLoader.java    |  17 +-
 .../sis/internal/map}/coverage/RenderingData.java  | 262 +++++++++++----------
 5 files changed, 266 insertions(+), 133 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index d689e89..c6887ec 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -181,12 +181,12 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * The {@code RenderedImage} to draw together with transform from pixel 
coordinates to display coordinates.
-     * Shall never be {@code null} but may be {@linkplain 
RenderingData#isEmpty() empty}. This instance shall be
-     * read and modified in JavaFX thread only and cloned if those data are 
needed by a background thread.
+     * Shall never be {@code null} but may be {@link 
StyledRenderingData#isEmpty() empty}. This instance shall
+     * be read and modified in JavaFX thread only and cloned if those data are 
needed by a background thread.
      *
      * @see Worker
      */
-    private RenderingData data;
+    private StyledRenderingData data;
 
     /**
      * The {@link #data} with different operations applied on them. Currently 
the only supported operation is
@@ -249,7 +249,7 @@ public class CoverageCanvas extends MapCanvasAWT {
      */
     CoverageCanvas(final Locale locale) {
         super(locale);
-        data                  = new RenderingData((report) -> errorReport = 
report.getDescription());
+        data                  = new StyledRenderingData((report) -> 
errorReport = report.getDescription());
         derivedImages         = new EnumMap<>(Stretching.class);
         resourceProperty      = new SimpleObjectProperty<>(this, "resource");
         coverageProperty      = new SimpleObjectProperty<>(this, "coverage");
@@ -659,7 +659,7 @@ public class CoverageCanvas extends MapCanvasAWT {
         /**
          * Value of {@link CoverageCanvas#data} at the time this worker has 
been initialized.
          */
-        private final RenderingData data;
+        private final StyledRenderingData data;
 
         /**
          * Value of {@link CoverageCanvas#getObjectiveCRS()} at the time this 
worker has been initialized.
@@ -722,7 +722,7 @@ public class CoverageCanvas extends MapCanvasAWT {
          * when the only change is a translation. But this transform may also 
contain a rotation or scale factor during
          * a short time if the rendering happens while {@link 
#prefetchedImage} is in need to be recomputed.
          *
-         * @see RenderingData#displayToObjective
+         * @see StyledRenderingData#displayToObjective
          */
         private AffineTransform resampledToDisplay;
 
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
index 79b25ae..10283fd 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
@@ -84,8 +84,8 @@ final class MultiResolutionImageLoader extends 
MultiResolutionCoverageLoader {
      * @return loader for the specified resource (never {@code null}).
      * @throws DataStoreException if an error occurred while querying the 
resource for resolutions.
      */
-    static MultiResolutionImageLoader getInstance(final GridCoverageResource 
resource,
-            MultiResolutionImageLoader cached) throws DataStoreException
+    static MultiResolutionCoverageLoader getInstance(final 
GridCoverageResource resource,
+            MultiResolutionCoverageLoader cached) throws DataStoreException
     {
         if (cached == null || cached.resource != resource) {
             synchronized (CACHE) {
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StyledRenderingData.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StyledRenderingData.java
new file mode 100644
index 0000000..30b02f3
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StyledRenderingData.java
@@ -0,0 +1,104 @@
+/*
+ * 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.gui.coverage;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.awt.image.RenderedImage;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.image.ErrorHandler;
+import org.apache.sis.internal.processing.image.Isolines;
+import org.apache.sis.internal.map.coverage.RenderingData;
+
+
+/**
+ * The {@code RenderedImage} to draw in a {@link CoverageCanvas} together with 
transform
+ * from pixel coordinates to display coordinates.
+ *
+ * This class extends the base {@code RenderingData} with additional visual 
components
+ * that are specific to {@link CoverageCanvas}. It may be replaced by a more 
generic
+ * renderer in this future. So this class is maybe temporary.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.1
+ * @module
+ */
+final class StyledRenderingData extends RenderingData {
+    /**
+     * Key of the currently selected alternative in {@link 
CoverageCanvas#derivedImages} map.
+     *
+     * @see #recolor()
+     */
+    Stretching selectedDerivative;
+
+    /**
+     * Creates a new instance initialized to no image.
+     *
+     * @param  errorHandler  where to report errors during tile computations.
+     */
+    StyledRenderingData(final ErrorHandler errorHandler) {
+        super(errorHandler);
+        selectedDerivative = Stretching.NONE;
+    }
+
+    /**
+     * Stretches the color ramp of source image according the current value of 
{@link #selectedDerivative}.
+     * This method uses the original image as the source of statistics. It 
saves computation time
+     * (no need to recompute the statistics when the projection is changed) 
and provides more stable
+     * visual output when standard deviations are used for configuring the 
color ramp.
+     *
+     * @return the source image with {@link #selectedDerivative} applied.
+     */
+    final RenderedImage recolor() throws DataStoreException {
+        RenderedImage image = getSourceImage();
+        if (selectedDerivative != Stretching.NONE) {
+            final Map<String,Object> modifiers = statistics();
+            if (selectedDerivative == Stretching.AUTOMATIC) {
+                modifiers.put("multStdDev", 3);
+            }
+            image = processor.stretchColorRamp(image, modifiers);
+        }
+        return image;
+    }
+
+    /**
+     * Prepares isolines by computing the the Java2D shapes that were not 
already computed in a previous rendering.
+     * This method shall be invoked in a background thread after image 
rendering has been completed (because this
+     * method uses some image computation results).
+     *
+     * @param  isolines  value of {@link IsolineRenderer#prepare()}, or {@code 
null} if none.
+     * @return result of isolines generation, or {@code null} if there is no 
isoline to compute.
+     * @throws TransformException if an interpolated point can not be 
transformed using the given transform.
+     */
+    final Future<Isolines[]> generate(final IsolineRenderer.Snapshot[] 
isolines) throws TransformException {
+        if (isolines == null) return null;
+        final MathTransform centerToObjective = 
getDataToObjective(PixelInCell.CELL_CENTER);
+        return IsolineRenderer.generate(isolines, getSourceImage(), 
centerToObjective);
+    }
+
+    /**
+     * Creates new rendering data initialized to a copy of this instance.
+     */
+    @Override
+    public StyledRenderingData clone() {
+        return (StyledRenderingData) super.clone();
+    }
+}
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
index 7c478a1..b205786 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
@@ -83,7 +83,7 @@ public class MultiResolutionCoverageLoader {
     /**
      * The resource from which to read grid coverages.
      */
-    protected final GridCoverageResource resource;
+    public final GridCoverageResource resource;
 
     /**
      * Squares of resolution at each pyramid level, from finest (smaller 
numbers) to coarsest (largest numbers).
@@ -190,6 +190,13 @@ public class MultiResolutionCoverageLoader {
     }
 
     /**
+     * Returns the maximal level (the level with coarsest resolution).
+     */
+    final int getLastLevel() {
+        return resolutionSquared.length - 1;
+    }
+
+    /**
      * Returns the pyramid level for a zoom defined by the given "objective to 
display" transform.
      * Only the scale factors of the given transform will be considered; 
translations are ignored.
      *
@@ -200,10 +207,10 @@ public class MultiResolutionCoverageLoader {
      * @return pyramid level for the zoom determined by the given transform. 
Finest level is 0.
      * @throws TransformException if an error occurred while computing 
resolution from given transforms.
      */
-    public final int findPyramidLevel(final MathTransform dataToObjective, 
final LinearTransform objectiveToDisplay,
-                                      final DirectPosition objectivePOI) 
throws TransformException
+    final int findPyramidLevel(final MathTransform dataToObjective, final 
LinearTransform objectiveToDisplay,
+                               final DirectPosition objectivePOI) throws 
TransformException
     {
-        int level = Math.max(resolutionSquared.length - 1, 0);
+        int level = getLastLevel();
         if (level != 0) {
             final LinearTransform displayToObjective = 
objectiveToDisplay.inverse();
             final Matrix m = displayToObjective.getMatrix();
@@ -353,7 +360,7 @@ dimensions: for (int j=0; j<tgtDim; j++) {
      */
     @Override
     public String toString() {
-        final int count = resolutionSquared.length - 1;
+        final int count = getLastLevel();
         double delta = magnitude(0);
         if (count != 0) {
             delta = (magnitude(count) - delta) / count;
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
similarity index 80%
rename from 
application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
rename to 
core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index 57a54b2..a4bc23a 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -14,12 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.gui.coverage;
+package org.apache.sis.internal.map.coverage;
 
 import java.util.Map;
-import java.util.HashMap;
 import java.util.List;
-import java.util.concurrent.Future;
+import java.util.HashMap;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.awt.Graphics2D;
@@ -43,8 +42,8 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.ImageRenderer;
-import org.apache.sis.coverage.grid.PixelTranslation;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.PixelTranslation;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
@@ -55,7 +54,6 @@ import org.apache.sis.internal.coverage.SampleDimensions;
 import org.apache.sis.internal.coverage.j2d.ColorModelType;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.referencing.WraparoundApplicator;
-import org.apache.sis.internal.processing.image.Isolines;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.math.Statistics;
@@ -69,11 +67,12 @@ import org.apache.sis.referencing.CRS;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.portrayal.PlanarCanvas;       // For javadoc.
 
 
 /**
- * The {@code RenderedImage} to draw in a {@link CoverageCanvas} together with 
transform
- * from pixel coordinates to display coordinates.
+ * The {@code RenderedImage} to draw in a {@link PlanarCanvas} together with 
transforms from pixel coordinates
+ * to display coordinates. This is a helper class for implementations of 
stateful renderer.
  *
  * <h2>Note on Java2D optimizations</h2>
  * {@link Graphics2D#drawRenderedImage(RenderedImage, AffineTransform)} 
implementation
@@ -83,7 +82,7 @@ import org.apache.sis.util.logging.Logging;
  *   <li>If the image is an instance of {@link BufferedImage},
  *       then the {@link AffineTransform} can be anything. Java2D applies 
interpolations efficiently.</li>
  *   <li>Otherwise if the {@link AffineTransform} scale factors are 1 and the 
translations are integers,
- *       then Java2D invokes {@link RenderedImage#getTile(int, int)}. It make 
possible for us to create
+ *       then Java2D invokes {@link RenderedImage#getTile(int, int)}. It makes 
possible for us to create
  *       a very large image covering the whole data but with tiles computed 
only when first requested.</li>
  *   <li>Otherwise Java2D invokes {@link RenderedImage#getData(Rectangle)}, 
which is more costly.
  *       We try to avoid that situation.</li>
@@ -101,7 +100,12 @@ import org.apache.sis.util.logging.Logging;
  * @since   1.1
  * @module
  */
-final class RenderingData implements Cloneable {
+public class RenderingData implements Cloneable {
+    /**
+     * The {@value} value, for identifying code that assume two-dimensional 
objects.
+     */
+    private static final int BIDIMENSIONAL = 2;
+
     /**
      * Whether to allow the creation of {@link 
java.awt.image.IndexColorModel}. This flag may be temporarily set
      * to {@code false} for testing or debugging. If {@code false}, images may 
be only grayscale and may be much
@@ -115,7 +119,7 @@ final class RenderingData implements Cloneable {
      * explicitly assigned to {@link #data}, or if the image may vary 
depending on the resolution.
      * The same instance may be shared by many {@link RenderingData} objects.
      */
-    MultiResolutionImageLoader coverageLoader;
+    public MultiResolutionCoverageLoader coverageLoader;
 
     /**
      * The pyramid level of {@linkplain #data} loaded by the {@linkplain 
#coverageLoader}.
@@ -126,12 +130,12 @@ final class RenderingData implements Cloneable {
      * The data fetched from {@link GridCoverage#render(GridExtent)} for 
current {@code sliceExtent}.
      * This rendered image may be tiled and fetching those tiles may require 
computations to be performed
      * in background threads. Pixels in this {@code data} image are mapped to 
pixels in the display
-     * {@link CoverageCanvas#image} by the following chain of operations:
+     * {@link PlanarCanvas} by the following chain of operations:
      *
      * <ol>
      *   <li><code>{@linkplain 
#dataGeometry}.getGridGeometry(CELL_CENTER)</code></li>
      *   <li><code>{@linkplain #changeOfCRS}.getMathTransform()</code></li>
-     *   <li>{@link CoverageCanvas#getObjectiveToDisplay()}</li>
+     *   <li>{@link PlanarCanvas#getObjectiveToDisplay()}</li>
      * </ol>
      *
      * This field is initially {@code null}.
@@ -140,6 +144,7 @@ final class RenderingData implements Cloneable {
      * @see #dataRanges
      * @see #isEmpty()
      * @see #loadIfNeeded(LinearTransform, DirectPosition, GridExtent)
+     * @see #getSourceImage()
      */
     private RenderedImage data;
 
@@ -147,7 +152,7 @@ final class RenderingData implements Cloneable {
      * Conversion from {@link #data} pixel coordinates to the coverage CRS, 
together with geospatial area.
      * It contains the {@link GridGeometry#getGridToCRS(PixelInCell)} value of 
{@link GridCoverage} reduced
      * to two dimensions and with a translation added for taking in account 
the requested {@code sliceExtent}.
-     * The coverage CRS is initially the same as the {@linkplain 
CoverageCanvas#getObjectiveCRS() objective CRS},
+     * The coverage CRS is initially the same as the {@linkplain 
PlanarCanvas#getObjectiveCRS() objective CRS},
      * but may become different later if user selects a different objective 
CRS.
      *
      * @see #data
@@ -160,21 +165,20 @@ final class RenderingData implements Cloneable {
      * Ranges of sample values in each band of {@link #data}. This is used for 
determining on which sample values
      * to apply colors when user asked to apply a color ramp. May be {@code 
null}.
      *
-     * @see #data
-     * @see #dataGeometry
      * @see #setCoverageSpace(GridGeometry, List)
+     * @see #statistics()
      */
     private List<SampleDimension> dataRanges;
 
     /**
-     * Conversion or transformation from {@linkplain #data} CRS to {@linkplain 
CoverageCanvas#getObjectiveCRS()
+     * Conversion or transformation from {@linkplain #data} CRS to {@linkplain 
PlanarCanvas#getObjectiveCRS()
      * objective CRS}, or {@code null} if not yet computed. This is an 
identity operation if the user did not
      * selected a different CRS after the coverage has been shown.
      */
     private CoordinateOperation changeOfCRS;
 
     /**
-     * Conversion from {@link #data} pixel coordinates to {@linkplain 
CoverageCanvas#getObjectiveCRS() objective CRS}.
+     * Conversion from {@link #data} pixel coordinates to {@linkplain 
PlanarCanvas#getObjectiveCRS() objective CRS}.
      * This is value of {@link GridGeometry#getGridToCRS(PixelInCell)} invoked 
on {@link #dataGeometry}, concatenated
      * with {@link #changeOfCRS} and potentially completed by a wraparound 
operation.
      * May be {@code null} if not yet computed.
@@ -182,7 +186,7 @@ final class RenderingData implements Cloneable {
     private MathTransform cornerToObjective;
 
     /**
-     * Conversion from {@linkplain CoverageCanvas#getObjectiveCRS() objective 
CRS} to {@link #data} pixel coordinates.
+     * Conversion from {@linkplain PlanarCanvas#getObjectiveCRS() objective 
CRS} to {@link #data} pixel coordinates.
      * This is the inverse of {@link #changeOfCRS} (potentially with a 
wraparound operation) concatenated with inverse
      * of {@link GridGeometry#getGridToCRS(PixelInCell)} on {@link 
#dataGeometry}.
      * May be {@code null} if not yet computed.
@@ -190,7 +194,7 @@ final class RenderingData implements Cloneable {
     private MathTransform objectiveToCenter;
 
     /**
-     * The inverse of the {@linkplain CoverageCanvas#objectiveToDisplay 
objective to display} transform which was
+     * The inverse of the {@linkplain PlanarCanvas#objectiveToDisplay 
objective to display} transform which was
      * active at the time resampled images have been computed. The 
concatenation of this transform with the actual
      * "objective to display" transform at the time the rendered image is 
drawn should be a translation.
      * May be {@code null} if not yet computed.
@@ -200,61 +204,49 @@ final class RenderingData implements Cloneable {
     private AffineTransform displayToObjective;
 
     /**
-     * Key of the currently selected alternative in {@link 
CoverageCanvas#derivedImages} map.
+     * Statistics on pixel values of current {@link #data}, or {@code null} if 
not yet computed.
+     * This is the cached value of {@link #statistics()}.
      *
-     * @see #recolor()
-     */
-    Stretching selectedDerivative;
-
-    /**
-     * Statistics on pixel values of current {@link #data}, or {@code null} if 
none or not yet computed.
-     * There is one {@link Statistics} instance per band. This is a cache for 
stretching the color ramp
-     * of the image to view. The {@link #recolor()} method uses statistics on 
the source image instead
-     * of statistics on the shown image in order to have stable colors during 
pans or zooms.
-     *
-     * @see #recolor()
+     * @see #statistics()
      */
     private Statistics[] statistics;
 
     /**
-     * The processor that we use for resampling image and stretching their 
color ramps.
+     * The processor that we use for resampling image and recoloring the image.
      */
-    final ImageProcessor processor;
+    public final ImageProcessor processor;
 
     /**
      * Creates a new instance initialized to no image.
      *
      * @param  errorHandler  where to report errors during tile computations.
      */
-    RenderingData(final ErrorHandler errorHandler) {
-        selectedDerivative = Stretching.NONE;
+    public RenderingData(final ErrorHandler errorHandler) {
         processor = new ImageProcessor();
         processor.setErrorHandler(errorHandler);
         processor.setImageResizingPolicy(ImageProcessor.Resizing.EXPAND);
     }
 
     /**
-     * Returns {@code true} if this object has no data.
-     * If {@code true}, then {@link CoverageCanvas} should paint
-     * an empty space without starting a background worker thread.
+     * Returns {@code true} if this object has no image to render.
+     *
+     * @return {@code true} if this object has no data.
      */
-    final boolean isEmpty() {
+    public final boolean isEmpty() {
         return data == null && dataGeometry == null && dataRanges == null;
     }
 
     /**
-     * Verifies if this {@code RenderingData} contains an image for the given 
objective CRS.
-     * If this is not the case, the cached resampled images will be discarded.
-     *
-     * @param  objectiveCRS  the coordinate reference system to use for 
rendering.
-     * @return whether the data are valid for the given objective CRS.
+     * Clears this renderer. This method should be invoked when the source of 
data (resource or coverage) changed.
+     * The {@link #displayToObjective} transform will be recomputed from 
scratch when first needed.
      */
-    final boolean validateCRS(final CoordinateReferenceSystem objectiveCRS) {
-        if (changeOfCRS != null && 
!Utilities.equalsIgnoreMetadata(objectiveCRS, changeOfCRS.getTargetCRS())) {
-            clearCRS();
-            return false;
-        }
-        return true;
+    public final void clear() {
+        clearCRS();
+        displayToObjective = null;
+        statistics         = null;
+        data               = null;
+        dataRanges         = null;
+        dataGeometry       = null;
     }
 
     /**
@@ -267,30 +259,31 @@ final class RenderingData implements Cloneable {
     }
 
     /**
-     * Clears this renderer. This method should be invoked when the source of 
data (resource or coverage) changed.
-     * The {@link #displayToObjective} transform will be recomputed from 
scratch when first needed.
+     * Verifies if this {@code RenderingData} contains an image for the given 
objective CRS.
+     * If this is not the case, the cached resampled images will need to be 
discarded.
+     *
+     * @param  objectiveCRS  the coordinate reference system to use for 
rendering.
+     * @return whether the data are valid for the given objective CRS.
      */
-    final void clear() {
-        clearCRS();
-        displayToObjective = null;
-        statistics         = null;
-        data               = null;
-        dataRanges         = null;
-        dataGeometry       = null;
+    public final boolean validateCRS(final CoordinateReferenceSystem 
objectiveCRS) {
+        if (changeOfCRS != null && 
!Utilities.equalsIgnoreMetadata(objectiveCRS, changeOfCRS.getTargetCRS())) {
+            clearCRS();
+            return false;
+        }
+        return true;
     }
 
     /**
      * Sets the input space (domain) and output space (ranges) of the coverage 
to be rendered.
      * It should be followed by a call to {@link 
#ensureImageLoaded(GridCoverage, GridExtent)}.
      *
-     * @param  data    the new image, or {@code null} if not yet known (i.e. 
loading may have been deferred).
      * @param  domain  the two-dimensional grid geometry, or {@code null} if 
there is no data.
      * @param  ranges  descriptions of bands, or {@code null} if there is no 
data.
      *
      * @see #isEmpty()
      */
     @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
-    final void setCoverageSpace(final GridGeometry domain, final 
List<SampleDimension> ranges) {
+    public final void setCoverageSpace(final GridGeometry domain, final 
List<SampleDimension> ranges) {
         dataRanges   = ranges;      // Not cloned because already an 
unmodifiable list.
         dataGeometry = domain;
         /*
@@ -322,10 +315,12 @@ final class RenderingData implements Cloneable {
      * @param  objectivePOI        point where to compute resolution, in 
coordinates of objective CRS.
      * @return the loaded grid coverage, or {@code null} if no loading has 
been done
      *         (which means that the coverage is unchanged, not that it does 
not exist).
+     * @throws TransformException if an error occurred while computing 
resolution from given transforms.
+     * @throws DataStoreException if an error occurred while loading the 
coverage.
      *
      * @see #setCoverageSpace(GridGeometry, List)
      */
-    final GridCoverage ensureCoverageLoaded(final LinearTransform 
objectiveToDisplay, final DirectPosition objectivePOI)
+    public final GridCoverage ensureCoverageLoaded(final LinearTransform 
objectiveToDisplay, final DirectPosition objectivePOI)
             throws TransformException, DataStoreException
     {
         final MathTransform dataToObjective = (changeOfCRS != null) ? 
changeOfCRS.getMathTransform() : null;
@@ -343,9 +338,13 @@ final class RenderingData implements Cloneable {
      * once after {@link #setCoverageSpace(GridGeometry, List)}. The {@code 
coverage} given in argument
      * may be the value returned by {@link 
#ensureCoverageLoaded(LinearTransform, DirectPosition)}.
      *
-     * @param  coverage  the coverage from which to read data, or {@code null} 
if the coverage did not changed.
+     * @param  coverage     the coverage from which to read data, or {@code 
null} if the coverage did not changed.
+     * @param  sliceExtent  a subspace of the grid coverage extent where all 
dimensions except two have a size of 1 cell.
+     *         May be {@code null} if this grid coverage has only two 
dimensions with a size greater than 1 cell.
+     * @throws FactoryException if the CRS changed but the transform from old 
to new CRS can not be determined.
+     * @throws TransformException if an error occurred while transforming 
coordinates from old to new CRS.
      */
-    final void ensureImageLoaded(final GridCoverage coverage, @Deprecated 
final GridExtent sliceExtent)
+    public final void ensureImageLoaded(final GridCoverage coverage, final 
GridExtent sliceExtent)
             throws FactoryException, TransformException
     {
         if (data != null || coverage == null) {
@@ -356,7 +355,7 @@ final class RenderingData implements Cloneable {
         final RenderedImage image = coverage.render(sliceExtent);
         final Object value = image.getProperty(PlanarImage.GRID_GEOMETRY_KEY);
         final GridGeometry domain = (value instanceof GridGeometry) ? 
(GridGeometry) value
-                : new ImageRenderer(coverage, 
sliceExtent).getImageGeometry(MultiResolutionImageLoader.BIDIMENSIONAL);
+                : new ImageRenderer(coverage, 
sliceExtent).getImageGeometry(BIDIMENSIONAL);
         setCoverageSpace(domain, ranges);
         data = image;
         /*
@@ -411,6 +410,15 @@ final class RenderingData implements Cloneable {
     }
 
     /**
+     * Returns the image which will be used as the source for rendering 
operations.
+     *
+     * @return the image loaded be {@link #ensureImageLoaded(GridCoverage, 
GridExtent)}.
+     */
+    protected final RenderedImage getSourceImage() {
+        return data;
+    }
+
+    /**
      * Returns the position at the center of source data, or {@code null} if 
none.
      */
     private DirectPosition getSourceMedian() {
@@ -421,28 +429,41 @@ final class RenderingData implements Cloneable {
     }
 
     /**
-     * Stretches the color ramp of source image according the current value of 
{@link #selectedDerivative}.
-     * This method uses the original image as the source of statistics. It 
saves computation time
-     * (no need to recompute the statistics when the projection is changed) 
and provides more stable
-     * visual output when standard deviations are used for configuring the 
color ramp.
+     * Returns statistics on the source image (computed when first requested, 
then cached).
+     * There is one {@link Statistics} instance per band. This is an 
information for dynamic
+     * stretching of image color ramp. Such recoloring operation should use 
statistics on the
+     * source image instead of statistics on the shown image in order to have 
stable colors
+     * during pans or zooms.
      *
-     * @return the given image with {@link #selectedDerivative} applied.
-     */
-    final RenderedImage recolor() {
-        RenderedImage image = data;
-        if (selectedDerivative != Stretching.NONE) {
-            final Map<String,Object> modifiers = new HashMap<>(4);
-            if (statistics == null) {
-                statistics = processor.valueOfStatistics(image, null, 
SampleDimensions.toSampleFilters(processor, dataRanges));
-            }
-            modifiers.put("statistics", statistics);
-            if (selectedDerivative == Stretching.AUTOMATIC) {
-                modifiers.put("multStdDev", 3);
+     * <p>The returned map is suitable for use with {@link 
ImageProcessor#stretchColorRamp(RenderedImage, Map)}.
+     * The map content is:</p>
+     * <ul>
+     *   <li>{@code "statistics"}: the statistics as a {@code Statistics[]} 
array.</li>
+     *   <li>{@code "sampleDimensions"}: band descriptions as a {@code 
List<SampleDimension>}.</li>
+     * </ul>
+     *
+     * This operation may be costly since it causes the loading of full image.
+     * If {@link #coverageLoader} is non-null, statistics will be computed on 
the
+     * image with coarsest resolution.
+     *
+     * @return statistics on sample values for each band, in a modifiable map.
+     * @throws DataStoreException if an error occurred while reading the image 
at coarsest resolution.
+     */
+    protected final Map<String,Object> statistics() throws DataStoreException {
+        if (statistics == null) {
+            RenderedImage image = data;
+            if (coverageLoader != null) {
+                final int level = coverageLoader.getLastLevel();
+                if (level != currentPyramidLevel) {
+                    image = 
coverageLoader.getOrLoad(level).forConvertedValues(true).render(null);
+                }
             }
-            modifiers.put("sampleDimensions", dataRanges);
-            image = processor.stretchColorRamp(image, modifiers);
+            statistics = processor.valueOfStatistics(image, null, 
SampleDimensions.toSampleFilters(processor, dataRanges));
         }
-        return image;
+        final Map<String,Object> modifiers = new HashMap<>(8);
+        modifiers.put("statistics", statistics);
+        modifiers.put("sampleDimensions", dataRanges);
+        return modifiers;
     }
 
     /**
@@ -456,9 +477,10 @@ final class RenderingData implements Cloneable {
      *   <li>{@link #processor} positional accuracy hint</li>
      * </ul>
      *
-     * @param  objectiveCRS  value of {@link CoverageCanvas#getObjectiveCRS()}.
+     * @param  objectiveCRS  value of {@link PlanarCanvas#getObjectiveCRS()}.
+     * @throws TransformException if an error occurred while transforming 
coordinates from grid to new CRS.
      */
-    final void setObjectiveCRS(final CoordinateReferenceSystem objectiveCRS) 
throws TransformException {
+    public final void setObjectiveCRS(final CoordinateReferenceSystem 
objectiveCRS) throws TransformException {
         if (changeOfCRS == null && objectiveCRS != null && 
dataGeometry.isDefined(GridGeometry.CRS)) try {
             changeOfCRS = 
CRS.findOperation(dataGeometry.getCoordinateReferenceSystem(), objectiveCRS,
                                             
dataGeometry.getGeographicExtent().orElse(null));
@@ -478,14 +500,15 @@ final class RenderingData implements Cloneable {
      * This method will compute the {@link MathTransform} steps from image 
coordinate system
      * to display coordinate system if those steps have not already been 
computed.
      *
-     * @param  recoloredImage      the image computed by {@link #recolor()}.
-     * @param  objectiveToDisplay  value of {@link 
CoverageCanvas#getObjectiveToDisplay()}.
-     * @param  objectivePOI        value of {@link 
CoverageCanvas#getPointOfInterest(boolean)} in objective CRS.
+     * @param  recoloredImage      {@link #data} or a derived (typically 
recolored) image.
+     * @param  objectiveToDisplay  value of {@link 
PlanarCanvas#getObjectiveToDisplay()}.
+     * @param  objectivePOI        value of {@link 
PlanarCanvas#getPointOfInterest(boolean)} in objective CRS.
      * @return image with operation applied and color ramp stretched.
+     * @throws TransformException if an error occurred in the use of "grid to 
CRS" transforms.
      */
-    final RenderedImage resampleAndConvert(final RenderedImage   
recoloredImage,
-                                           final LinearTransform 
objectiveToDisplay,
-                                           final DirectPosition  objectivePOI)
+    public final RenderedImage resampleAndConvert(final RenderedImage   
recoloredImage,
+                                                  final LinearTransform 
objectiveToDisplay,
+                                                  final DirectPosition  
objectivePOI)
             throws TransformException
     {
         /*
@@ -562,7 +585,7 @@ final class RenderingData implements Cloneable {
     }
 
     /**
-     * Conversion or transformation from {@linkplain 
CoverageCanvas#getObjectiveCRS() objective CRS} to
+     * Conversion or transformation from {@linkplain 
PlanarCanvas#getObjectiveCRS() objective CRS} to
      * {@linkplain #data} CRS. This transform will include {@code 
WraparoundTransform} steps if needed.
      */
     private static MathTransform applyWraparound(final MathTransform 
transform, final DirectPosition sourceMedian,
@@ -630,8 +653,8 @@ final class RenderingData implements Cloneable {
      * @param  displayBounds       size and location of the display device 
(plus margin), in pixel units.
      * @return a temporary image with tiles intersecting the display region 
already computed.
      */
-    final RenderedImage prefetch(final RenderedImage resampledImage, final 
AffineTransform resampledToDisplay,
-                                 final Envelope2D displayBounds)
+    public final RenderedImage prefetch(final RenderedImage resampledImage, 
final AffineTransform resampledToDisplay,
+                                        final Envelope2D displayBounds)
     {
         final Rectangle areaOfInterest;
         try {
@@ -644,12 +667,15 @@ final class RenderingData implements Cloneable {
     }
 
     /**
-     * Gets the transform to use for painting the stretched image. If the 
image to draw is an instance of
+     * Gets the transform to use for painting the resampled image. If the 
image to draw is an instance of
      * {@link BufferedImage}, then it is okay to have any transform. However 
for other kinds of image,
      * it is important that the transform has scale factors of 1 and integer 
translations because Java2D
      * has an optimization which avoid to copy the whole data only for that 
case.
+     *
+     * @param  objectiveToDisplay  the transform from objective CRS to canvas 
coordinates.
+     * @return transform from resampled image to canvas (display) coordinates.
      */
-    final AffineTransform getTransform(final LinearTransform 
objectiveToDisplay) {
+    public final AffineTransform getTransform(final LinearTransform 
objectiveToDisplay) {
         if (displayToObjective == null) {
             return new AffineTransform();
         }
@@ -668,7 +694,7 @@ final class RenderingData implements Cloneable {
      * @param  objectivePOI  point of interest in objective CRS.
      * @return an estimation of the source pixel size at the given location.
      */
-    final float getDataPixelSize(final DirectPosition objectivePOI) {
+    public final float getDataPixelSize(final DirectPosition objectivePOI) {
         if (objectiveToCenter != null) try {
             final Matrix d = objectiveToCenter.derivative(objectivePOI);
             double sum = 0;
@@ -689,13 +715,24 @@ final class RenderingData implements Cloneable {
     }
 
     /**
+     * Returns the conversion from {@link #data} pixel coordinates to
+     * {@linkplain PlanarCanvas#getObjectiveCRS() objective CRS}.
+     *
+     * @param  anchor  whether the conversion should start from pixel corner 
or pixel center.
+     * @return conversion from data pixel coordinates to objective CRS.
+     */
+    public final MathTransform getDataToObjective(final PixelInCell anchor) {
+        return PixelTranslation.translate(cornerToObjective, 
PixelInCell.CELL_CORNER, anchor);
+    }
+
+    /**
      * Converts the given bounds from objective coordinates to pixel 
coordinates in the source coverage.
      *
      * @param  bounds  objective coordinates.
      * @return data coverage cell coordinates (in pixels), or {@code null} if 
unknown.
      * @throws TransformException if the bounds can not be transformed.
      */
-    final Rectangle objectiveToData(final Rectangle2D bounds) throws 
TransformException {
+    public final Rectangle objectiveToData(final Rectangle2D bounds) throws 
TransformException {
         if (objectiveToCenter == null) return null;
         return (Rectangle) 
Shapes2D.transform(MathTransforms.bidimensional(objectiveToCenter), bounds, new 
Rectangle());
     }
@@ -703,8 +740,11 @@ final class RenderingData implements Cloneable {
     /**
      * Returns whether {@link #dataGeometry} or {@link #objectiveToCenter} 
changed since a previous rendering.
      * This is used for information purposes only.
+     *
+     * @param  previous  previous instance of {@code RenderingData}.
+     * @return whether this {@code RenderingData} does a different rendering 
than previous {@code RenderingData}.
      */
-    final boolean hasChanged(final RenderingData previous) {
+    public final boolean hasChanged(final RenderingData previous) {
         /*
          * Really !=, not Object.equals(Object), because we rely on new 
instances to be created
          * (even if equal) as a way to detect that cached values have not been 
reused.
@@ -714,26 +754,10 @@ final class RenderingData implements Cloneable {
 
     /**
      * Invoked when an exception occurred while computing a transform but the 
painting process can continue.
-     * This method pretends that the warning come from {@link CoverageCanvas} 
class since it is the public API.
+     * This method pretends that the warning come from {@link PlanarCanvas} 
class since it is the public API.
      */
     private static void recoverableException(final Exception e) {
-        Logging.recoverableException(Logging.getLogger(Modules.APPLICATION), 
CoverageCanvas.class, "render", e);
-    }
-
-    /**
-     * Prepares isolines by computing the the Java2D shapes that were not 
already computed in a previous rendering.
-     * This method shall be invoked in a background thread after image 
rendering has been completed (because this
-     * method uses some image computation results).
-     *
-     * @param  isolines  value of {@link IsolineRenderer#prepare()}, or {@code 
null} if none.
-     * @return result of isolines generation, or {@code null} if there is no 
isoline to compute.
-     * @throws TransformException if an interpolated point can not be 
transformed using the given transform.
-     */
-    final Future<Isolines[]> generate(final IsolineRenderer.Snapshot[] 
isolines) throws TransformException {
-        if (isolines == null) return null;
-        final MathTransform centerToObjective = PixelTranslation.translate(
-                cornerToObjective, PixelInCell.CELL_CORNER, 
PixelInCell.CELL_CENTER);
-        return IsolineRenderer.generate(isolines, data, centerToObjective);
+        Logging.recoverableException(Logging.getLogger(Modules.PORTRAYAL), 
PlanarCanvas.class, "render", e);
     }
 
     /**
@@ -751,8 +775,6 @@ final class RenderingData implements Cloneable {
     /**
      * Returns a string representation for debugging purposes.
      * The string content may change in any future version.
-     *
-     * @see CoverageCanvas#toString()
      */
     @Override
     public String toString() {

Reply via email to