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.

Reply via email to