sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Clarify ComputedImage assumptions on pixel coordinate system.
Date Mon, 06 Jan 2020 11:41:12 GMT
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 <martin.desruisseaux@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.


Mime
View raw message