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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 6380604  Begin support of WritableRenderedImage in ComputedImage.
6380604 is described below

commit 6380604aa43515d495b7b693139a1e8584b51ac6
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Jan 6 20:15:59 2020 +0100

    Begin support of WritableRenderedImage in ComputedImage.
---
 .../sis/coverage/grid/ConvertedGridCoverage.java   |   2 +-
 .../apache/sis/coverage/grid/GridCoverage2D.java   |   2 +-
 .../java/org/apache/sis/image/ComputedImage.java   | 159 +++++++++++++++++----
 .../main/java/org/apache/sis/image/TileCache.java  |   8 ++
 .../coverage/j2d/BandedSampleConverter.java        | 112 ++++++++++++---
 .../coverage/j2d/BandedSampleConverterTest.java    |   2 +-
 6 files changed, 235 insertions(+), 50 deletions(-)

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 531052d..1a9450b 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
@@ -218,7 +218,7 @@ final class ConvertedGridCoverage extends GridCoverage {
          */
         if (image != null) {
             final ColorModel colorModel = createColorModel(VISIBLE_BAND, 
dataType);
-            image = new BandedSampleConverter(image, null, dataType, 
colorModel, converters);
+            image = BandedSampleConverter.create(image, null, dataType, 
colorModel, converters);
         }
         return image;
     }
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 4990ff1..f205ec6 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
@@ -148,7 +148,7 @@ public class GridCoverage2D extends GridCoverage {
         super(source.gridGeometry, range);
         final int dataType = ConvertedGridCoverage.getDataType(range, 
isConverted);
         final ColorModel colorModel = 
createColorModel(ConvertedGridCoverage.VISIBLE_BAND, dataType);
-        data           = new BandedSampleConverter(source.data, null, 
dataType, colorModel, converters);
+        data           = BandedSampleConverter.create(source.data, null, 
dataType, colorModel, converters);
         gridToImageX   = source.gridToImageX;
         gridToImageY   = source.gridToImageY;
         xDimension     = source.xDimension;
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index c11eb71..88217b3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -18,6 +18,8 @@ package org.apache.sis.image;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Vector;
 import java.awt.Insets;
@@ -124,26 +126,19 @@ public abstract class ComputedImage extends PlanarImage {
     public static final String SOURCE_PADDING_PROPERTY = "sourcePadding";
 
     /**
-     * Whether a tile in the cache is ready for use or needs to be recomputed
-     * because one if its sources changed its data.
+     * Whether a tile in the cache is ready for use or needs to be recomputed 
because one if its sources
+     * changed its data. If the tile is checkout out for a write operation, 
the write operation will have
+     * precedence over the dirty state.
      */
     private enum TileStatus {
-        /** The tile is ready for use. */
-        STORED,
+        /** The tile, if present, is ready for use. */
+        VALID,
 
         /** The tile needs to be recomputed because at least one source 
changed its data. */
         DIRTY,
 
         /** The tile has been checked out for a write operation. */
-        CHECKED,
-
-        /** The tile needs to be recomputed, but it is also checked for write 
operation by someone else. */
-        CHECKED_AND_DIRTY;
-
-        /** Remapping function for calls to {@link 
Map#computeIfPresent(Object, java.util.function.BiFunction)}. */
-        static TileStatus dirty(final TileCache.Key key, TileStatus oldValue) {
-            return (oldValue == CHECKED) ? CHECKED_AND_DIRTY : DIRTY;
-        }
+        WRITABLE
     }
 
     /**
@@ -192,24 +187,46 @@ public abstract class ComputedImage extends PlanarImage {
         }
 
         /**
-         * Remembers that the given tile will need to be removed from the cache
-         * when the enclosing image will be garbage-collected.
+         * Sets the status of the specified tile, discarding any previous 
status.
+         */
+        final void setTileStatus(final TileCache.Key key, final TileStatus 
status) {
+            synchronized (cachedTiles) {
+                cachedTiles.put(key, status);
+            }
+        }
+
+        /**
+         * Returns the status of the tile at the specified indices.
+         * The main status of interest are:
+         * <ul>
+         *   <li>{@link TileStatus#DIRTY}    — if the tile needs to be 
recomputed.</li>
+         *   <li>{@link TileStatus#WRITABLE} — if the tile is currently 
checked out for writing.</li>
+         * </ul>
          */
-        final void addTile(final TileCache.Key key) {
+        final TileStatus getTileStatus(final TileCache.Key key) {
             synchronized (cachedTiles) {
-                cachedTiles.put(key, TileStatus.STORED);
+                return cachedTiles.get(key);
             }
         }
 
         /**
-         * Returns {@code true} if the specified tile needs to be recomputed.
+         * Adds in the given list the indices of all tiles which are checked 
out for writing.
+         * If the given list is {@code null}, then this method stops the 
search at the first
+         * tile checked out.
+         *
+         * @param  indices  the list where to add indices, or {@code null} if 
none.
+         * @return whether at least one tile is checked out for writing.
          */
-        final boolean isDirty(final TileCache.Key key) {
-            final TileStatus status;
+        final boolean getWritableTileIndices(final List<Point> indices) {
             synchronized (cachedTiles) {
-                status = cachedTiles.get(key);
+                for (final Map.Entry<TileCache.Key, TileStatus> entry : 
cachedTiles.entrySet()) {
+                    if (entry.getValue() == TileStatus.WRITABLE) {
+                        if (indices == null) return true;
+                        indices.add(entry.getKey().indices());
+                    }
+                }
             }
-            return (status == TileStatus.DIRTY) || (status == 
TileStatus.CHECKED_AND_DIRTY);
+            return (indices != null) && !indices.isEmpty();
         }
 
         /**
@@ -224,7 +241,7 @@ public abstract class ComputedImage extends PlanarImage {
                 for (int tileY = minTileY; tileY <= maxTileY; tileY++) {
                     for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
                         final TileCache.Key key = new TileCache.Key(this, 
tileX, tileY);
-                        cachedTiles.computeIfPresent(key, TileStatus::dirty);
+                        cachedTiles.replace(key, TileStatus.VALID, 
TileStatus.DIRTY);
                     }
                 }
             }
@@ -497,6 +514,13 @@ public abstract class ComputedImage extends PlanarImage {
      *       If an error occurred, an {@link ImagingOpException} is 
thrown.</li>
      * </ol>
      *
+     * <h4>Race conditions with write operations</h4>
+     * If this image implements the {@link WritableRenderedImage} interface, 
then a user may have acquired
+     * the tile for a write operation outside the {@link #computeTile 
computeTile(…)} method. In such case,
+     * there is no consistency guarantees on sample values: the tile returned 
by this method may show data
+     * in an unspecified stage during the write operation. This situation may 
be detected by checking if
+     * {@link #isTileWritable(int, int) isTileWritable(tileX, tileY)} returns 
{@code true}.
+     *
      * @param  tileX  the column index of the tile to get.
      * @param  tileY  the row index of the tile to get.
      * @return the tile at the given index (never null).
@@ -508,14 +532,14 @@ public abstract class ComputedImage extends PlanarImage {
         final TileCache.Key key = new TileCache.Key(reference, tileX, tileY);
         final Cache<TileCache.Key,Raster> cache = TileCache.GLOBAL;
         Raster tile = cache.peek(key);
-        if (tile == null || reference.isDirty(key)) {
+        if (tile == null || reference.getTileStatus(key) == TileStatus.DIRTY) {
             int min;
             ArgumentChecks.ensureBetween("tileX", (min = getMinTileX()), min + 
getNumXTiles() - 1, tileX);
             ArgumentChecks.ensureBetween("tileY", (min = getMinTileY()), min + 
getNumYTiles() - 1, tileY);
             final Cache.Handler<Raster> handler = cache.lock(key);
             try {
                 tile = handler.peek();
-                if (tile == null || reference.isDirty(key)) {
+                if (tile == null || reference.getTileStatus(key) == 
TileStatus.DIRTY) {
                     final WritableRaster previous = (tile instanceof 
WritableRaster) ? (WritableRaster) tile : null;
                     Exception cause = null;
                     tile = null;
@@ -530,7 +554,7 @@ public abstract class ComputedImage extends PlanarImage {
                         throw (ImagingOpException) new 
ImagingOpException(Resources.format(
                                 Resources.Keys.CanNotComputeTile_2, tileX, 
tileY)).initCause(cause);
                     }
-                    reference.addTile(key);
+                    reference.setTileStatus(key, TileStatus.VALID);
                 }
             } finally {
                 handler.putAndUnlock(tile);     // Must be invoked even if an 
exception occurred.
@@ -580,6 +604,89 @@ public abstract class ComputedImage extends PlanarImage {
     }
 
     /**
+     * Returns whether any tile is checked out for writing.
+     * This method always returns {@code false} for read-only images, but may 
return {@code true}
+     * if this {@code ComputedImage} is also a {@link WritableRenderedImage}.
+     *
+     * @return {@code true} if any tiles are checked out for writing; {@code 
false} otherwise.
+     *
+     * @see #markWritableTile(int, int, boolean)
+     * @see WritableRenderedImage#hasTileWriters()
+     */
+    public boolean hasTileWriters() {
+        return reference.getWritableTileIndices(null);
+    }
+
+    /**
+     * Returns whether a tile is currently checked out for writing.
+     * This method always returns {@code false} for read-only images, but may 
return {@code true}
+     * if this {@code ComputedImage} is also a {@link WritableRenderedImage}.
+     *
+     * @param  tileX the X index of the tile.
+     * @param  tileY the Y index of the tile.
+     * @return {@code true} if specified tile is checked out for writing; 
{@code false} otherwise.
+     *
+     * @see #markWritableTile(int, int, boolean)
+     * @see WritableRenderedImage#isTileWritable(int, int)
+     */
+    public boolean isTileWritable(final int tileX, final int tileY) {
+        return reference.getTileStatus(new TileCache.Key(reference, tileX, 
tileY)) == TileStatus.WRITABLE;
+    }
+
+    /**
+     * Returns an array of Point objects indicating which tiles are checked 
out for writing, or {@code null} if none.
+     * This method always returns {@code null} for read-only images, but may 
return a non-empty array
+     * if this {@code ComputedImage} is also a {@link WritableRenderedImage}.
+     *
+     * @return an array containing the locations of tiles that are checked out 
for writing, or {@code null} if none.
+     *
+     * @see #markWritableTile(int, int, boolean)
+     * @see WritableRenderedImage#getWritableTileIndices()
+     */
+    public Point[] getWritableTileIndices() {
+        final List<Point> indices = new ArrayList<>();
+        if (reference.getWritableTileIndices(indices)) {
+            return indices.toArray(new Point[indices.size()]);
+        }
+        return null;
+    }
+
+    /**
+     * Marks a tile as checkout out for writing. This method is provided for 
subclasses that also implement
+     * the {@link WritableRenderedImage} interface. This method can be used as 
below:
+     *
+     * {@preformat java
+     *     class MyImage extends ComputedImage implements 
WritableRenderedImage {
+     *         // Constructor omitted for brevity.
+     *
+     *         &#64;Override
+     *         public WritableRaster getWritableTile(int tileX, int tileY) {
+     *             WritableRaster raster = ...;             // Get the 
writable tile here.
+     *             markWritableTile(tileX, tileY, true);
+     *             return raster;
+     *         }
+     *
+     *         &#64;Override
+     *         public void releaseWritableTile(int tileX, int tileY) {
+     *             markWritableTile(tileX, tileY, false);
+     *             // Release the raster here.
+     *         }
+     *     }
+     * }
+     *
+     * @param  tileX    the X index of the tile to acquire or release.
+     * @param  tileY    the Y index of the tile to acquire or release.
+     * @param  writing  {@code true} for acquiring the tile, or {@code false} 
for releasing it.
+     *
+     * @see WritableRenderedImage#getWritableTile(int, int)
+     * @see WritableRenderedImage#releaseWritableTile(int, int)
+     */
+    protected void markWritableTile(final int tileX, final int tileY, final 
boolean writing) {
+        final TileCache.Key key = new TileCache.Key(reference, tileX, tileY);
+        reference.setTileStatus(key, writing ? TileStatus.WRITABLE : 
TileStatus.VALID);
+    }
+
+    /**
      * Marks all tiles in the given range of indices as in need of being 
recomputed.
      * The tiles will not be recomputed immediately, but only on next 
invocation of
      * {@link #getTile(int, int) getTile(tileX, tileY)} if the {@code (tileX, 
tileY)} indices
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/TileCache.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/TileCache.java
index 7562101..ce49d25 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/TileCache.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/TileCache.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.image;
 
+import java.awt.Point;
 import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.lang.ref.Reference;
@@ -100,6 +101,13 @@ final class TileCache extends Cache<TileCache.Key, Raster> 
{
         }
 
         /**
+         * Returns the tile indices.
+         */
+        final Point indices() {
+            return new Point(tileX, tileY);
+        }
+
+        /**
          * Removes the raster associated to this key. This method is invoked
          * for all tiles in an image being disposed.
          */
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java
index a7cdca4..de3ddaa 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java
@@ -20,13 +20,17 @@ import java.awt.Dimension;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
 import java.awt.image.RenderedImage;
+import java.awt.image.WritableRenderedImage;
 import java.awt.image.BandedSampleModel;
 import java.awt.image.ColorModel;
+import java.awt.image.TileObserver;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.image.ComputedImage;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.Workaround;
+import org.apache.sis.util.logging.Logging;
 
 
 /**
@@ -44,15 +48,16 @@ import org.apache.sis.util.Workaround;
  *   <li>Calculation is performed on {@code float} or {@code double} 
numbers.</li>
  * </ul>
  *
- * Subclasses may relax those restrictions at the cost of more complex {@link 
#computeTile(int, int, WritableRaster)}
- * implementation. Those restrictions may also be relaxed in future versions 
of this class.
+ * If the given source is writable and the transform are invertible, then the 
{@code BandedSampleConverter}
+ * returned by the {@link #create create(…)} method will implement {@link 
WritableRenderedImage} interface.
+ * In such case, writing converted values will cause the corresponding source 
values to be updated too.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
-public final class BandedSampleConverter extends ComputedImage {
+public class BandedSampleConverter extends ComputedImage {
     /**
      * The transfer functions to apply on each band of the source image.
      */
@@ -64,6 +69,22 @@ public final class BandedSampleConverter extends 
ComputedImage {
     private final ColorModel colorModel;
 
     /**
+     * Creates a new image which will compute values using the given 
converters.
+     *
+     * @param  source       the image for which to convert sample values.
+     * @param  sampleModel  the sample model shared by all tiles in this image.
+     * @param  colorModel   the color model for from the expected range of 
values, or {@code null}.
+     * @param  converters   the transfer functions to apply on each band of 
the source image.
+     */
+    BandedSampleConverter(final RenderedImage source,  final BandedSampleModel 
sampleModel,
+                          final ColorModel colorModel, final MathTransform1D[] 
converters)
+    {
+        super(sampleModel, source);
+        this.colorModel = colorModel;
+        this.converters = converters;
+    }
+
+    /**
      * Creates a new image of the given data type which will compute values 
using the given converters.
      * The number of bands is the length of the {@code converters} array, 
which must be greater than 0
      * and not greater than the number of bands in the source image.
@@ -73,31 +94,38 @@ public final class BandedSampleConverter extends 
ComputedImage {
      * @param  targetType  the type of this image resulting from conversion of 
given image.
      * @param  colorModel  the color model for from the expected range of 
values, or {@code null}.
      * @param  converters  the transfer functions to apply on each band of the 
source image.
+     * @return the image which compute converted values from the given source.
      */
-    public BandedSampleConverter(final RenderedImage source, final ImageLayout 
layout,
-                                 final int targetType, final ColorModel 
colorModel,
-                                 final MathTransform1D... converters)
-    {
-        super(createSampleModel(targetType, converters.length, layout, 
source), source);
-        this.colorModel = colorModel;
-        this.converters = converters;
-    }
-
-    /**
-     * Returns the sample model to use for this image. This is a workaround 
for RFE #4093999
-     * ("Relax constraint on placement of this()/super() call in 
constructors").
-     */
-    @Workaround(library="JDK", version="1.8")
-    private static BandedSampleModel createSampleModel(final int targetType,
-            final int numBands, ImageLayout layout, final RenderedImage source)
+    public static BandedSampleConverter create(final RenderedImage source, 
ImageLayout layout,
+                                               final int targetType, final 
ColorModel colorModel,
+                                               final MathTransform1D... 
converters)
     {
         ArgumentChecks.ensureNonNull("source", source);
+        ArgumentChecks.ensureNonNull("converters", converters);
+        final int numBands = converters.length;
         ArgumentChecks.ensureSizeBetween("converters", 1, 
source.getSampleModel().getNumBands(), numBands);
         if (layout == null) {
             layout = ImageLayout.DEFAULT;
         }
         final Dimension tile = layout.suggestTileSize(source);
-        return RasterFactory.unique(new BandedSampleModel(targetType, 
tile.width, tile.height, numBands));
+        final BandedSampleModel sampleModel = RasterFactory.unique(
+                new BandedSampleModel(targetType, tile.width, tile.height, 
numBands));
+        /*
+         * If the source image is writable, then changes in the converted 
image may be retro-propagated
+         * to that source image. If we fail to compute the required inverse 
transforms, log a notice at
+         * a low level because this is not a serious problem; writable 
BandedSampleConverter is a plus
+         * but not a requirement.
+         */
+        if (source instanceof WritableRenderedImage) try {
+            final MathTransform1D[] inverses = new MathTransform1D[numBands];
+            for (int i=0; i<numBands; i++) {
+                inverses[i] = converters[i].inverse();
+            }
+            return new Writable((WritableRenderedImage) source, sampleModel, 
colorModel, converters, inverses);
+        } catch (NoninvertibleTransformException e) {
+            Logging.recoverableException(Logging.getLogger(Modules.RASTER), 
BandedSampleConverter.class, "create", e);
+        }
+        return new BandedSampleConverter(source, sampleModel, colorModel, 
converters);
     }
 
     /**
@@ -195,4 +223,46 @@ public final class BandedSampleConverter extends 
ComputedImage {
         Transferer.create(getSource(0), target).compute(converters);
         return target;
     }
+
+    /**
+     * A {@code BandedSampleConverter} capable to retro-propagate the changes 
to the source coverage.
+     */
+    private static final class Writable extends BandedSampleConverter 
implements WritableRenderedImage {
+        /**
+         * The converters for computing the source values from a converted 
value.
+         */
+        private final MathTransform1D[] inverses;
+
+        /**
+         * Creates a new writable image which will compute values using the 
given converters.
+         */
+        Writable(final WritableRenderedImage source,  final BandedSampleModel 
sampleModel,
+                 final ColorModel colorModel, final MathTransform1D[] 
converters, final MathTransform1D[] inverses)
+        {
+            super(source, sampleModel, colorModel, converters);
+            this.inverses = inverses;
+        }
+
+        @Override
+        public void addTileObserver(final TileObserver observer) {
+        }
+
+        @Override
+        public void removeTileObserver(final TileObserver observer) {
+        }
+
+        @Override
+        public WritableRaster getWritableTile(final int tileX, final int 
tileY) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void releaseWritableTile(final int tileX, final int tileY) {
+        }
+
+        @Override
+        public void setData(final Raster data) {
+            throw new UnsupportedOperationException();
+        }
+    }
 }
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverterTest.java
 
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverterTest.java
index f443450..95e3cf0 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverterTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverterTest.java
@@ -70,7 +70,7 @@ public final strictfp class BandedSampleConverterTest extends 
TestCase {
                 random.nextInt(20) - 10);       // minTileY
         source.validate();
         source.initializeAllTiles(0);
-        image = new BandedSampleConverter(source, null, targetType, null,
+        image = BandedSampleConverter.create(source, null, targetType, null,
                 (MathTransform1D) MathTransforms.linear(scale, 0));
     }
 

Reply via email to