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 0859cb94d509b970b5afbc176698a42789556e1a 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..2025215 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 Math.max(resolutionSquared.length - 1, 0); + } + + /** * 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() {