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.
+ *
+ * @Override
+ * public WritableRaster getWritableTile(int tileX, int tileY) {
+ * WritableRaster raster = ...; // Get the
writable tile here.
+ * markWritableTile(tileX, tileY, true);
+ * return raster;
+ * }
+ *
+ * @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));
}