You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2020/01/06 11:41:12 UTC
[sis] branch geoapi-4.0 updated: Clarify ComputedImage assumptions
on pixel coordinate system.
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 <ma...@geomatys.com>
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.