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 22ea241 ComputedImage tracks changes in its WritableRenderedImage
sources in order to know which tiles to recompute.
22ea241 is described below
commit 22ea241f174c5e19b53786b7164aa6bbf247d938
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Jan 5 17:50:53 2020 +0100
ComputedImage tracks changes in its WritableRenderedImage sources in order
to know which tiles to recompute.
---
.../java/org/apache/sis/image/ComputedImage.java | 248 ++++++++++++++++++---
.../coverage/j2d/BandedSampleConverter.java | 13 +-
.../org/apache/sis/image/ComputedImageTest.java | 2 +-
.../java/org/apache/sis/image/TiledImageMock.java | 3 +-
4 files changed, 222 insertions(+), 44 deletions(-)
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 aa6f19c..b6ba21f 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
@@ -16,21 +16,25 @@
*/
package org.apache.sis.image;
-import java.util.Set;
-import java.util.HashSet;
+import java.util.Map;
+import java.util.HashMap;
import java.util.Arrays;
import java.util.Vector;
import java.awt.Point;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
-import java.awt.image.ImagingOpException;
+import java.awt.image.WritableRenderedImage;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
+import java.awt.image.TileObserver;
+import java.awt.image.ImagingOpException;
import java.lang.ref.WeakReference;
import org.apache.sis.internal.system.ReferenceQueueConsumer;
import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.collection.Cache;
import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Disposable;
@@ -43,9 +47,10 @@ import org.apache.sis.util.Disposable;
*
* <p>Subclasses need to implement at least the following methods:</p>
* <ul>
- * <li>{@link #getWidth()} — the image width in pixels.</li>
- * <li>{@link #getHeight()} — the image height in pixels.</li>
- * <li>{@link #computeTile(int,int)} — invoked when a requested tile is not
in the cache.</li>
+ * <li>{@link #getWidth()} — the image width in pixels.</li>
+ * <li>{@link #getHeight()} — the image height in pixels.</li>
+ * <li>{@link #computeTile(int, int, WritableRaster)} — invoked when a
+ * requested tile is not in the cache or needs to be updated.</li>
* </ul>
*
* <p>If pixel coordinates or tile indices do not start at zero,
@@ -66,24 +71,74 @@ import org.apache.sis.util.Disposable;
*/
public abstract class ComputedImage extends PlanarImage {
/**
+ * Whether a tile in the cache is ready for use or needs to be recomputed
+ * because one if its sources changed its data.
+ */
+ private enum TileStatus {
+ /** The tile is ready for use. */
+ STORED,
+
+ /** 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#merge(Object, Object,
java.util.function.BiFunction)}. */
+ static TileStatus merge(final TileStatus oldValue, TileStatus
newValue) {
+ if (newValue == DIRTY && oldValue == CHECKED) {
+ newValue = CHECKED_AND_DIRTY;
+ }
+ return newValue;
+ }
+ }
+
+ /**
* Weak reference to the enclosing image together with necessary
information for releasing resources
* when image is disposed. This class shall not contain any strong
reference to the enclosing image.
*/
// MUST be static
- private static final class Cleaner extends WeakReference<ComputedImage>
implements Disposable {
+ private static final class Cleaner extends WeakReference<ComputedImage>
implements Disposable, TileObserver {
/**
* Indices of all cached tiles. Used for removing tiles from the cache
when the image is disposed.
* All accesses to this collection must be synchronized. This field
has to be declared here because
* {@link Cleaner} is not allowed to keep a strong reference to the
enclosing {@link ComputedImage}.
*/
- private final Set<TileCache.Key> cachedTiles;
+ private final Map<TileCache.Key, TileStatus> cachedTiles;
/**
- * Creates a new weak reference to the given image.
+ * All {@link ComputedImage#sources} that are writable, or {@code
null} if none.
+ * This is used for removing tile observers when the enclosing image
is garbage-collected.
*/
- Cleaner(final ComputedImage image) {
+ private WritableRenderedImage[] sources;
+
+ /**
+ * Creates a new weak reference to the given image and registers this
{@link Cleaner}
+ * as a listener of all given sources. The listeners will be
automatically removed
+ * when the enclosing image is garbage collected.
+ *
+ * @param image the enclosing image for which to release tiles on
garbage-collection.
+ * @param ws sources to observe for changes, or {@code null} if
none.
+ */
+ @SuppressWarnings("ThisEscapedInObjectConstruction")
+ Cleaner(final ComputedImage image, final WritableRenderedImage[] ws) {
super(image, ReferenceQueueConsumer.QUEUE);
- cachedTiles = new HashSet<>();
+ cachedTiles = new HashMap<>();
+ sources = ws;
+ if (ws != null) {
+ int i = 0;
+ try {
+ while (i < ws.length) {
+ WritableRenderedImage source = ws[i++]; // `i++`
must be before `addTileObserver(…)` call.
+ source.addTileObserver(this);
+ }
+ } catch (RuntimeException e) {
+ unregister(ws, i, e); //
`unregister(…)` will rethrow the given exception.
+ }
+ }
}
/**
@@ -92,23 +147,110 @@ public abstract class ComputedImage extends PlanarImage {
*/
final void addTile(final TileCache.Key key) {
synchronized (cachedTiles) {
- cachedTiles.add(key);
+ cachedTiles.put(key, TileStatus.STORED);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the specified tile needs to be recomputed.
+ */
+ final boolean isDirty(final TileCache.Key key) {
+ final TileStatus status;
+ synchronized (cachedTiles) {
+ status = cachedTiles.get(key);
+ }
+ return (status == TileStatus.DIRTY) || (status ==
TileStatus.CHECKED_AND_DIRTY);
+ }
+
+ /**
+ * Invoked when a source is changing the content of one of its tile.
+ * This method is interested only in events fired after the change is
done.
+ * The tiles that depend on the modified tile are marked in need to be
recomputed.
+ *
+ * @param source the image that own the tile which is about
to be updated.
+ * @param tileX the <var>x</var> index of the tile that is
being updated.
+ * @param tileY the <var>y</var> index of the tile that is
being updated.
+ * @param willBeWritable if true, the tile is grabbed for writing;
otherwise it is being released.
+ */
+ @Override
+ public void tileUpdate(final WritableRenderedImage source, int tileX,
int tileY, final boolean willBeWritable) {
+ if (!willBeWritable) {
+ final ComputedImage target = get();
+ if (target != null) {
+ final long sourceWidth = source.getTileWidth();
+ final long sourceHeight = source.getTileHeight();
+ final long targetWidth = target.getTileWidth();
+ final long targetHeight = target.getTileHeight();
+ final long tx = tileX * sourceWidth +
source.getTileGridXOffset() - target.getTileGridXOffset();
+ final long ty = tileY * sourceHeight +
source.getTileGridYOffset() - target.getTileGridYOffset();
+ final int maxTileX = Numerics.clamp(Math.floorDiv(tx
+ sourceWidth - 1, targetWidth));
+ final int maxTileY = Numerics.clamp(Math.floorDiv(ty
+ sourceHeight - 1, targetHeight));
+ final int minTileX = Numerics.clamp(Math.floorDiv(tx,
targetWidth));
+ final int minTileY = Numerics.clamp(Math.floorDiv(ty,
targetHeight));
+ synchronized (cachedTiles) {
+ for (tileY = minTileY; tileY <= maxTileY; tileY++) {
+ for (tileX = minTileX; tileX <= maxTileX; tileX++)
{
+ final TileCache.Key key = new
TileCache.Key(this, tileX, tileY);
+ cachedTiles.merge(key, TileStatus.DIRTY,
TileStatus::merge);
+ }
+ }
+ }
+ } else {
+ /*
+ * Should not happen, unless maybe the source invoked this
method before `dispose()`
+ * has done its work. Or maybe we have a bug in our code
and this `Cleaner` is still
+ * alive but should not. In any cases there is no point to
continue observing the source.
+ */
+ source.removeTileObserver(this);
+ }
}
}
/**
* Invoked when the enclosing image has been garbage-collected. This
method removes all cached tiles
- * that were owned by the enclosing image. This method should not
perform other cleaning work than
- * removing cached tiles because it is not guaranteed to be invoked if
{@link TileCache#GLOBAL}
- * does not contain any tile for the enclosing image (because there
would be nothing preventing
- * this weak reference to be garbage collected before {@code
dispose()} is invoked).
+ * that were owned by the enclosing image and unregister all tile
observers.
+ *
+ * This method should not perform other cleaning work because it is
not guaranteed to be invoked if
+ * this {@code Cleaner} is not registered as a {@link TileObserver}
and if {@link TileCache#GLOBAL}
+ * does not contain any tile for the enclosing image. The reason is
because there would be nothing
+ * preventing this weak reference to be garbage collected before
{@code dispose()} is invoked.
+ *
+ * @see ComputedImage#dispose()
*/
@Override
public void dispose() {
synchronized (cachedTiles) {
- cachedTiles.forEach(TileCache.Key::dispose);
+ cachedTiles.keySet().forEach(TileCache.Key::dispose);
cachedTiles.clear();
}
+ final WritableRenderedImage[] ws = sources;
+ if (ws != null) {
+ unregister(ws, ws.length, null);
+ }
+ }
+
+ /**
+ * Stops observing writable sources for modifications. This methods is
invoked when the enclosing
+ * image is garbage collected. It may also be invoked for rolling back
observer registrations if
+ * an error occurred during {@link Cleaner} construction. This method
clears the {@link #sources}
+ * field immediately for letting the garbage collector to collect the
sources in the event where
+ * this {@code Cleaner} would live longer than expected.
+ *
+ * @param ws a copy of {@link #sources}. Can not be null.
+ * @param i index after the last source to stop observing.
+ * @param failure if this method is invoked because an exception
occurred, that exception.
+ */
+ private void unregister(final WritableRenderedImage[] ws, int i,
RuntimeException failure) {
+ sources = null; // Let GC to its work in case
of error in this method.
+ while (--i >= 0) try {
+ ws[i].removeTileObserver(this);
+ } catch (RuntimeException e) {
+ if (failure == null) failure = e;
+ else failure.addSuppressed(e);
+ }
+ if (failure != null) {
+ throw failure;
+ }
}
}
@@ -143,11 +285,10 @@ public abstract class ComputedImage extends PlanarImage {
/**
* Creates an initially empty image with the given sample model.
- * The default tile size will be the width and height of the given sample
model.
- *
- * <div class="note"><b>Note:</b>
- * the restriction about sample model size matching tile size is for
reducing the amount
- * of memory consumed by {@link #createTile(int, int)}.</div>
+ * The default tile size will be the width and height of the given sample
model
+ * (this default setting minimizes the amount of memory consumed by {@link
#createTile(int, int)}).
+ * This constructor automatically registers a {@link TileObserver}
+ * for all sources that are {@link WritableRenderedImage} instances.
*
* @param sampleModel the sample model shared by all tiles in this image.
* @param sources sources of this image (may be an empty array), or
a null array if unknown.
@@ -155,14 +296,33 @@ public abstract class ComputedImage extends PlanarImage {
protected ComputedImage(final SampleModel sampleModel, RenderedImage...
sources) {
ArgumentChecks.ensureNonNull("sampleModel", sampleModel);
this.sampleModel = sampleModel;
+ /*
+ * Verify the `sources` argument validity and opportunistically
collect all writable sources
+ * in a separated array. If at the end it appears that the two arrays
have the same content,
+ * the same array will be shared by this `ComputedImage` and its
`TileObserver`.
+ */
+ WritableRenderedImage[] ws = null;
if (sources != null) {
sources = sources.clone();
+ int count = 0;
for (int i=0; i<sources.length; i++) {
- ArgumentChecks.ensureNonNullElement("sources", i, sources[i]);
+ final RenderedImage source = sources[i];
+ ArgumentChecks.ensureNonNullElement("sources", i, source);
+ if (source instanceof WritableRenderedImage) {
+ if (ws == null) {
+ ws = new WritableRenderedImage[sources.length - i];
+ }
+ ws[count++] = (WritableRenderedImage) source;
+ }
+ }
+ if (count == sources.length) {
+ sources = ws; // The two arrays have the
same content; share the same one.
+ } else {
+ ws = ArraysExt.resize(ws, count);
}
}
this.sources = sources; // Note: null value does not have
same meaning than empty array.
- reference = new Cleaner(this); // Create cleaner last after all
arguments have been validated.
+ reference = new Cleaner(this, ws); // Create cleaner last after all
arguments have been validated.
}
/**
@@ -256,17 +416,19 @@ 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) {
+ if (tile == null || reference.isDirty(key)) {
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) {
+ if (tile == null || reference.isDirty(key)) {
+ final WritableRaster previous = (tile instanceof
WritableRaster) ? (WritableRaster) tile : null;
Exception cause = null;
+ tile = null;
try {
- tile = computeTile(tileX, tileY);
+ tile = computeTile(tileX, tileY, previous);
} catch (ImagingOpException e) {
throw e; // Let that kind
of exception propagate.
} catch (Exception e) {
@@ -286,19 +448,33 @@ public abstract class ComputedImage extends PlanarImage {
}
/**
- * Invoked when a tile need to be computed. This method is invoked by
{@link #getTile(int, int)}
- * when the requested tile is not in the cache. The returned tile will be
automatically cached.
+ * Invoked when a tile need to be computed or updated. This method is
invoked by {@link #getTile(int, int)}
+ * when the requested tile is not in the cache, or when a writable source
notified us that its data changed.
+ * The returned tile will be automatically cached.
+ *
+ * <p>A typical implementation is as below:</p>
+ * {@preformat java
+ * @Override
+ * protected Raster computeTile(int tileX, int tileY, WritableRaster
tile) {
+ * if (tile == null) {
+ * tile = createTile(tileX, tileY);
+ * }
+ * // Do calculation here and write results in tile.
+ * return tile;
+ * }
+ * }
*
- * @param tileX the column index of the tile to compute.
- * @param tileY the row index of the tile to compute.
- * @return computed tile for the given indices (can not be null).
+ * @param tileX the column index of the tile to compute.
+ * @param tileY the row index of the tile to compute.
+ * @param previous if the tile already exists but needs to be updated,
the tile to update. Otherwise {@code null}.
+ * @return computed tile for the given indices. May be the {@code
previous} tile after update but can not be null.
* @throws Exception if an error occurred while computing the tile.
*/
- protected abstract Raster computeTile(int tileX, int tileY) throws
Exception;
+ protected abstract Raster computeTile(int tileX, int tileY, WritableRaster
previous) throws Exception;
/**
* Creates an initially empty tile at the given tile grid position.
- * This is a helper method for {@link #computeTile(int, int)}
implementations.
+ * This is a helper method for {@link #computeTile(int, int,
WritableRaster)} implementations.
*
* @param tileX the column index of the tile to create.
* @param tileY the row index of the tile to create.
@@ -312,8 +488,8 @@ public abstract class ComputedImage extends PlanarImage {
}
/**
- * Advises this image that its tiles will no longer be requested.
- * This method removes all tiles from the cache.
+ * Advises this image that its tiles will no longer be requested. This
method removes all
+ * tiles from the cache and stops observation of {@link
WritableRenderedImage} sources.
* This image should not be used anymore after this method call.
*
* <p><b>Note:</b> keep in mind that this image may be referenced as a
source of other images.
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 4f87725..c488436 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
@@ -44,7 +44,7 @@ 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)}
+ * 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.
*
* @author Martin Desruisseaux (Geomatys)
@@ -171,14 +171,17 @@ public final class BandedSampleConverter extends
ComputedImage {
/**
* Computes the tile at specified indices.
*
- * @param tileX the column index of the tile to compute.
- * @param tileY the row index of the tile to compute.
+ * @param tileX the column index of the tile to compute.
+ * @param tileY the row index of the tile to compute.
+ * @param target if the tile already exists but needs to be updated, the
tile to update. Otherwise {@code null}.
* @return computed tile for the given indices (can not be null).
* @throws TransformException if an error occurred while converting a
sample value.
*/
@Override
- protected Raster computeTile(final int tileX, final int tileY) throws
TransformException {
- final WritableRaster target = createTile(tileX, tileY);
+ protected Raster computeTile(final int tileX, final int tileY,
WritableRaster target) throws TransformException {
+ if (target == null) {
+ target = createTile(tileX, tileY);
+ }
Transferer.create(getSource(0), target).compute(converters);
return target;
}
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/image/ComputedImageTest.java
b/core/sis-feature/src/test/java/org/apache/sis/image/ComputedImageTest.java
index 2c6e613..b4d34f5 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ComputedImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ComputedImageTest.java
@@ -60,7 +60,7 @@ public final strictfp class ComputedImageTest extends
TestCase {
@Override public ColorModel getColorModel() {return
getSource(0).getColorModel();}
@Override public int getWidth() {return
getSource(0).getWidth();}
@Override public int getHeight() {return
getSource(0).getHeight();}
- @Override protected Raster computeTile(final int tileX, final int
tileY) {
+ @Override protected Raster computeTile(final int tileX, final int
tileY, WritableRaster previous) {
final int tw = getTileWidth();
final int th = getTileHeight();
return getSource(0).getData(new Rectangle(tileX * tw, tileY *
th, tw, th));
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
b/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
index 9db5992..0df0cf8 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
@@ -288,11 +288,10 @@ public final strictfp class TiledImageMock extends
PlanarImage implements Writab
}
/**
- * Unsupported since we do not need tile observers for the tests.
+ * Ignored since we do not need tile observers for the tests.
*/
@Override
public void addTileObserver(TileObserver to) {
- throw new UnsupportedOperationException();
}
/**