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 5d08bd4 Clarify ComputedImage assumptions on pixel coordinate system.
5d08bd4 is described below
commit 5d08bd497fe3a71475d23aca590f1cccf4652fc3
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Jan 6 12:39:28 2020 +0100
Clarify ComputedImage assumptions on pixel coordinate system.
---
.../java/org/apache/sis/image/ComputedImage.java | 223 +++++++++++++++++----
1 file changed, 182 insertions(+), 41 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 b6ba21f..c11eb71 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
@@ -20,7 +20,9 @@ import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.Vector;
+import java.awt.Insets;
import java.awt.Point;
+import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
@@ -36,15 +38,45 @@ 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;
+import org.apache.sis.coverage.grid.GridExtent; // For javadoc
/**
* An image with tiles computed on-the-fly and cached for future reuse.
- * Computations are performed on a tile-by-tile basis and the result is
- * stored in a cache shared by all images on the platform. Tiles may be
- * discarded at any time, in which case they will need to be recomputed
- * when needed again.
+ * Computations are performed on a tile-by-tile basis (potentially in
different threads)
+ * and the results are stored in a cache shared by all images in the runtime
environment.
+ * Tiles may be discarded at any time or may become dirty if a source has been
modified,
+ * in which case those tiles will be recomputed when needed again.
*
+ * <p>{@code ComputedImage} may have an arbitrary number of source images,
including zero.
+ * A {@link TileObserver} is automatically registered to all sources that are
instances of
+ * {@link WritableRenderedImage}. If one of those sources sends a change
event, then all
+ * {@code ComputedImage} tiles that may be impacted by that change are marked
as <cite>dirty</cite>
+ * and will be computed again when needed.</p>
+ *
+ * <p>When this {@code ComputedImage} is garbage collected, all cached tiles
are discarded
+ * and the above-cited {@link TileObserver} is automatically removed from all
sources.
+ * This cleanup can be requested without waiting for garbage collection by
invoking the
+ * {@link #dispose()} method, but that call should be done only if the caller
is certain
+ * that this {@code ComputedImage} will not be used anymore.</p>
+ *
+ * <h2>Pixel coordinate system</h2>
+ * Default implementation assumes that the pixel in upper-left left corner is
located at coordinates (0,0).
+ * This assumption is consistent with {@link
org.apache.sis.coverage.grid.GridCoverage#render(GridExtent)}
+ * contract, which produces an image located at (0,0) when the image region
matches the {@code GridExtent}.
+ * However subclasses can use a non-zero origin by overriding the methods
documented in the
+ * <cite>Sub-classing</cite> section below.
+ *
+ * <p>If this {@code ComputedImage} does not have any {@link
WritableRenderedImage} source, then there is
+ * no other assumption on the pixel coordinate system. But if there is
writable sources, then the default
+ * implementation assumes that source images occupy the same region as this
{@code ComputedImage}:
+ * all pixels at coordinates (<var>x</var>, <var>y</var>) in this {@code
ComputedImage} depend on pixels
+ * at the same (<var>x</var>, <var>y</var>) coordinates in the source images,
+ * possibly expanded to neighborhood pixels as described in {@link
#SOURCE_PADDING_PROPERTY}.
+ * If this assumption does not hold, then subclasses should override the
+ * {@link #sourceTileChanged(RenderedImage, int, int)} method.</p>
+ *
+ * <h2>Sub-classing</h2>
* <p>Subclasses need to implement at least the following methods:</p>
* <ul>
* <li>{@link #getWidth()} — the image width in pixels.</li>
@@ -62,8 +94,6 @@ import org.apache.sis.util.Disposable;
* <li>{@link #getMinTileY()} — the minimum tile index in the <var>y</var>
direction.</li>
* </ul>
*
- * <p>This class is thread-safe: multiple tiles may be computed in different
background threads.</p>
- *
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
@@ -71,6 +101,29 @@ import org.apache.sis.util.Disposable;
*/
public abstract class ComputedImage extends PlanarImage {
/**
+ * The property for declaring the amount of additional source pixels
needed on each side of a destination pixel.
+ * This property can be used for calculations that require only a fixed
rectangular source region around a source
+ * pixel in order to compute each destination pixel. A given destination
pixel (<var>x</var>, <var>y</var>) may be
+ * computed from the neighborhood of source pixels beginning at
+ * (<var>x</var> - {@link Insets#left},
+ * <var>y</var> - {@link Insets#top}) and extending to
+ * (<var>x</var> + {@link Insets#right},
+ * <var>y</var> + {@link Insets#bottom}) inclusive.
+ * Those {@code left}, {@code top}, {@code right} and {@code bottom}
attributes can be positive, zero or negative,
+ * but their sums shall be positive with ({@code left} + {@code right}) ≥
0 and ({@code top} + {@code bottom}) ≥ 0.
+ *
+ * <p>The property value shall be an instance of {@link Insets} or {@code
Insets[]}.
+ * The array form can be used when a different padding is required for
each source image.
+ * In that case, the image source index is used as the index for accessing
the {@link Insets} element in the array.
+ * Null or {@linkplain java.awt.Image#UndefinedProperty undefined}
elements mean that no padding is applied.
+ * If the array length is shorter than the number of source images,
missing elements are considered as null.</p>
+ *
+ * @see #getProperty(String)
+ * @see #sourceTileChanged(RenderedImage, int, int)
+ */
+ 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.
*/
@@ -87,12 +140,9 @@ public abstract class ComputedImage extends PlanarImage {
/** 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;
+ /** 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;
}
}
@@ -142,7 +192,7 @@ public abstract class ComputedImage extends PlanarImage {
}
/**
- * Remember that the given tile will need to be removed from the cache
+ * Remembers that the given tile will need to be removed from the cache
* when the enclosing image will be garbage-collected.
*/
final void addTile(final TileCache.Key key) {
@@ -163,6 +213,24 @@ public abstract class ComputedImage extends PlanarImage {
}
/**
+ * Marks all tiles in the given range of indices as in need of being
recomputed.
+ * This method is invoked when some tiles of at least one source image
changed.
+ * All arguments, including maximum values, are inclusive.
+ *
+ * @see ComputedImage#markDirtyTiles(Rectangle)
+ */
+ final void markDirtyTiles(final int minTileX, final int minTileY,
final int maxTileX, final int maxTileY) {
+ synchronized (cachedTiles) {
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
* 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.
@@ -170,31 +238,14 @@ public abstract class ComputedImage extends PlanarImage {
* @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.
+ * @param willBeWritable if {@code 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);
- }
- }
- }
+ target.sourceTileChanged(source, tileX, tileY);
} else {
/*
* Should not happen, unless maybe the source invoked this
method before `dispose()`
@@ -208,7 +259,7 @@ public abstract class ComputedImage extends PlanarImage {
/**
* Invoked when the enclosing image has been garbage-collected. This
method removes all cached tiles
- * that were owned by the enclosing image and unregister all tile
observers.
+ * that were owned by the enclosing image and stops observing all
sources.
*
* 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}
@@ -233,7 +284,7 @@ public abstract class ComputedImage extends PlanarImage {
* 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
+ * field immediately for allowing the garbage collector to release 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.
@@ -258,12 +309,14 @@ public abstract class ComputedImage extends PlanarImage {
* Weak reference to this image, also used as a cleaner when the image is
garbage-collected.
* This reference is retained in {@link TileCache#GLOBAL}. Note that if
that cache does not
* cache any tile for this image, then this {@link Cleaner} may be
garbage-collected in same
- * time than this image and its {@link Cleaner#dispose()} method never
invoked.
+ * time than this image and its {@link Cleaner#dispose()} method may never
be invoked.
*/
private final Cleaner reference;
/**
- * The sources of this image, or {@code null} if unknown.
+ * The sources of this image, or {@code null} if unknown. This array
contains all sources.
+ * By contrast the {@link Cleaner#sources} array contains only the
modifiable sources, for
+ * which we listen for changes.
*
* @see #getSource(int)
*/
@@ -315,10 +368,17 @@ public abstract class ComputedImage extends PlanarImage {
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);
+ /*
+ * If `count` is 0, then `ws` is null while `sources` is non-null.
This is intentional:
+ * a null `sources` array does not have the same meaning than an
empty `sources` array.
+ * In the case of `ws` however, the difference does not matter so
we keep it to null.
+ */
+ if (count != 0) {
+ if (count == sources.length) {
+ sources = ws; // The two arrays have the
same content; share the same array.
+ } else {
+ ws = ArraysExt.resize(ws, count);
+ }
}
}
this.sources = sources; // Note: null value does not have
same meaning than empty array.
@@ -350,6 +410,38 @@ public abstract class ComputedImage extends PlanarImage {
}
/**
+ * Returns the property of the given name if it is of the given type, or
{@code null} otherwise.
+ * If the property value depends on the source image, then it can be an
array of type {@code T[]},
+ * in which case this method will return the element at the source index.
+ *
+ * @param <T> compile-tile value of {@code type} argument.
+ * @param type class of the property to get.
+ * @param name name of the property to get.
+ * @param source the source image if the property may depend on the
source.
+ * @return requested property if it is an instance of the specified type,
or {@code null} otherwise.
+ */
+ @SuppressWarnings("unchecked")
+ private <T> T getProperty(final Class<T> type, final String name, final
RenderedImage source) {
+ Object value = getProperty(name);
+ if (type.isInstance(value)) {
+ return (T) value;
+ }
+ if (sources != null && value instanceof Object[]) {
+ final Object[] array = (Object[]) value;
+ final int n = Math.min(sources.length, array.length);
+ for (int i=0; i<n; i++) {
+ if (sources[i] == source) {
+ value = array[i];
+ if (type.isInstance(value)) {
+ return (T) value;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
* Returns the sample model associated with this image.
* All rasters returned from this image will have this sample model.
* In {@code ComputedImage} implementation, the sample model determines
the tile size
@@ -397,7 +489,7 @@ public abstract class ComputedImage extends PlanarImage {
* This method performs the first of the following actions that apply:
*
* <ol>
- * <li>If the requested tile is present in the cache, then that tile is
returned immediately.</li>
+ * <li>If the requested tile is present in the cache and is not dirty,
then that tile is returned immediately.</li>
* <li>Otherwise if the requested tile is being computed in another
thread, then this method blocks
* until the other thread completed its work and returns its result.
If the other thread failed
* to compute the tile, an {@link ImagingOpException} is thrown.</li>
@@ -488,6 +580,55 @@ public abstract class ComputedImage extends PlanarImage {
}
/**
+ * 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
+ * are {@linkplain Rectangle#contains(int, int) contained} if the
specified rectangle.
+ *
+ * <p>Subclasses can invoke this method when the tiles in the given range
depend on source data
+ * that changed, typically (but not necessarily) {@linkplain #getSources()
source images}.
+ * Note that there is no need to invoke this method if the source images
are instances of
+ * {@link WritableRenderedImage}, because {@code ComputedImage} already
has {@link TileObserver}
+ * for them.</p>
+ *
+ * @param tiles indices of tiles to mark as dirty.
+ */
+ protected void markDirtyTiles(final Rectangle tiles) {
+ reference.markDirtyTiles(tiles.x, tiles.y,
+ Math.addExact(tiles.x, tiles.width - 1),
+ Math.addExact(tiles.y, tiles.height - 1));
+ }
+
+ /**
+ * Invoked when a tile of a source image has been updated. This method
should {@linkplain #markDirtyTiles
+ * mark as dirty} all tiles of this {@code ComputedImage} that depend on
the updated tile.
+ *
+ * <p>The default implementation assumes that source images use pixel
coordinate systems aligned with this
+ * {@code ComputedImage} in such a way that all pixels at coordinates
(<var>x</var>, <var>y</var>) in the
+ * {@code source} image are used for calculation of pixels at the same
(<var>x</var>, <var>y</var>) coordinates
+ * in this {@code ComputedImage}, possibly expanded to neighborhood pixels
if the {@value #SOURCE_PADDING_PROPERTY}
+ * property is defined. If this assumption does not hold, then subclasses
should override this method and invoke
+ * {@link #markDirtyTiles(Rectangle)} themselves.</p>
+ *
+ * @param source the image that own the tile which has been updated.
+ * @param tileX the <var>x</var> index of the tile that has been updated.
+ * @param tileY the <var>y</var> index of the tile that has been updated.
+ */
+ protected void sourceTileChanged(final RenderedImage source, final int
tileX, final int tileY) {
+ final long sourceWidth = source.getTileWidth();
+ final long sourceHeight = source.getTileHeight();
+ final long targetWidth = this .getTileWidth();
+ final long targetHeight = this .getTileHeight();
+ final long tx = tileX * sourceWidth +
source.getTileGridXOffset() - getTileGridXOffset();
+ final long ty = tileY * sourceHeight +
source.getTileGridYOffset() - getTileGridYOffset();
+ final Insets b = getProperty(Insets.class, SOURCE_PADDING_PROPERTY,
source);
+ reference.markDirtyTiles(Numerics.clamp(Math.floorDiv(tx - (b == null
? 0 : b.left), targetWidth)),
+ Numerics.clamp(Math.floorDiv(ty - (b == null
? 0 : b.top), targetHeight)),
+ Numerics.clamp(Math.floorDiv(tx + (b == null
? 0 : b.right) + sourceWidth - 1, targetWidth)),
+ Numerics.clamp(Math.floorDiv(ty + (b == null
? 0 : b.bottom) + sourceHeight - 1, targetHeight)));
+ }
+
+ /**
* 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.