This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit b2c6b5ee0268de08cc18100ddc446ba92bb63e24
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jul 31 16:29:21 2026 +0200

    Rename `ObservableImage` as `WritableUntiledImage` and merge with 
`ImageRenderer.Untiled`.
    The goal is to avoid incorrect inheritance of the `GridGeometry` property 
in sub-images.
---
 .../sis/coverage/grid/GridCoverageBuilder.java     |   4 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    | 119 +++----------
 .../main/org/apache/sis/image/BandSelectImage.java |   4 +-
 .../apache/sis/image/WritableComputedImage.java    |   8 +-
 .../sis/image/internal/shared/ImageUtilities.java  |   2 +-
 .../sis/image/internal/shared/RasterFactory.java   |   4 +-
 .../image/internal/shared/WritableTiledImage.java  |  14 +-
 ...ervableImage.java => WritableUntiledImage.java} | 193 +++++++++++++++------
 .../sis/storage/sql/postgis/RasterReader.java      |   4 +-
 .../org/apache/sis/storage/esri/RasterStore.java   |   4 +-
 .../sis/storage/image/internal/BufferedImages.java |   6 +-
 11 files changed, 185 insertions(+), 177 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index 67f59f1d3c..484967f95e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -43,7 +43,7 @@ import org.apache.sis.image.ImageLayout;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.WritablePixelIterator;
 import org.apache.sis.image.internal.shared.ColorScaleBuilder;
-import org.apache.sis.image.internal.shared.ObservableImage;
+import org.apache.sis.image.internal.shared.WritableUntiledImage;
 import org.apache.sis.image.internal.shared.TiledImage;
 import org.apache.sis.image.internal.shared.WritableTiledImage;
 import org.apache.sis.coverage.SampleDimension;
@@ -601,7 +601,7 @@ public class GridCoverageBuilder {
         if (colors != null && tiles.length == 1) {
             final Raster tile = tiles[0];
             if (tile instanceof WritableRaster && (tile.getMinX() | 
tile.getMinY()) == 0) {
-                return new ObservableImage(colors, (WritableRaster) tile, 
false, properties);
+                return new WritableUntiledImage(colors, (WritableRaster) tile, 
false, properties);
             }
         }
         if (tiles instanceof WritableRaster[]) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ImageRenderer.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ImageRenderer.java
index d78dd3b258..b5990beb96 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ImageRenderer.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -27,10 +27,8 @@ import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
-import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
-import java.awt.image.ImagingOpException;
 import java.awt.image.RasterFormatException;
 import java.awt.image.Raster;
 import static java.lang.Math.addExact;
@@ -48,13 +46,12 @@ import org.apache.sis.coverage.Category;
 import org.apache.sis.image.internal.shared.ColorScaleBuilder;
 import org.apache.sis.image.internal.shared.DeferredProperty;
 import org.apache.sis.image.internal.shared.RasterFactory;
-import org.apache.sis.image.internal.shared.ObservableImage;
 import org.apache.sis.image.internal.shared.TiledImage;
 import org.apache.sis.image.internal.shared.WritableTiledImage;
+import org.apache.sis.image.internal.shared.WritableUntiledImage;
 import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
-import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Vector;
 import static org.apache.sis.image.PlanarImage.XY_DIMENSIONS_KEY;
@@ -103,7 +100,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
  * Support for tiled images will be added in a future version.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.7
  *
  * @see GridCoverage#render(GridExtent)
  *
@@ -281,6 +278,7 @@ public class ImageRenderer {
     /**
      * The properties to give to the image, or {@code null} if none.
      *
+     * @see #properties()
      * @see #addProperty(String, Object)
      */
     @SuppressWarnings("UseOfObsoleteCollectionType")
@@ -512,10 +510,7 @@ public class ImageRenderer {
             case GRID_GEOMETRY_KEY:
             case SAMPLE_DIMENSIONS_KEY: break;
             default: {
-                if (properties == null) {
-                    properties = new Hashtable<>();
-                }
-                if (properties.putIfAbsent(key, value) == null) {
+                if (properties().putIfAbsent(key, value) == null) {
                     return;
                 }
             }
@@ -523,6 +518,17 @@ public class ImageRenderer {
         throw new 
IllegalArgumentException(Errors.format(Errors.Keys.ElementAlreadyPresent_1, 
key));
     }
 
+    /**
+     * Returns the properties, created when first needed.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    private Hashtable<String, Object> properties() {
+        if (properties == null) {
+            properties = new Hashtable<>();
+        }
+        return properties;
+    }
+
     /**
      * Returns {@code true} if a {@link #getImageGeometry(int)} request for 
the given number of CRS dimensions
      * can return {@link #geometry} directly. This common case avoids the need 
for more costly computation with
@@ -780,104 +786,19 @@ public class ImageRenderer {
                 supplier = new SliceGeometry(geometry, sliceExtent, 
gridDimensions, mtFactory);
             }
         }
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
+        final Hashtable<String, Object> properties = properties();
+        properties.putIfAbsent(SAMPLE_DIMENSIONS_KEY, bands);
+        properties.putIfAbsent(XY_DIMENSIONS_KEY, gridDimensions);
         final WritableRaster wr = (raster instanceof WritableRaster) ? 
(WritableRaster) raster : null;
         if (wr != null && cm != null && (imageX | imageY) == 0) {
-            return new Untiled(cm, wr, properties, gridDimensions, 
imageGeometry, supplier, bands);
+            return new WritableUntiledImage(cm, wr, false, 
properties).setGridGeometry(imageGeometry, supplier);
         }
-        if (properties == null) {
-            properties = new Hashtable<>();
-        }
-        properties.putIfAbsent(XY_DIMENSIONS_KEY, gridDimensions);
         properties.putIfAbsent(GRID_GEOMETRY_KEY, (supplier != null) ? new 
DeferredProperty(supplier) : imageGeometry);
-        properties.putIfAbsent(SAMPLE_DIMENSIONS_KEY, bands);
         if (wr != null) {
             return new WritableTiledImage(properties, cm, width, height, 0, 0, 
wr);
         } else {
             return new TiledImage(properties, cm, width, height, 0, 0, raster);
         }
     }
-
-    /**
-     * A {@link BufferedImage} which will compute the {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}
-     * property when first needed. We use this class even when the property 
value is known in advance because it
-     * has the desired side-effect of not letting {@link #getSubimage(int, 
int, int, int)} inherit that property.
-     * The use of a {@link BufferedImage} subclass is desired because Java2D 
rendering pipeline has optimizations
-     * in the form {@code if (image instanceof BufferedImage)}.
-     */
-    private static final class Untiled extends ObservableImage {
-        /**
-         * The value associated to the {@value 
org.apache.sis.image.PlanarImage#XY_DIMENSIONS_KEY} key.
-         */
-        private final int[] gridDimensions;
-
-        /**
-         * The value associated to the {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} key,
-         * or {@code null} if not yet computed.
-         */
-        private GridGeometry geometry;
-
-        /**
-         * The object to use for computing {@link #geometry}, or {@code null} 
if not needed.
-         * This field is cleared after {@link #geometry} has been computed.
-         */
-        private SliceGeometry supplier;
-
-        /**
-         * The value associated to the {@value 
org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY} key.
-         */
-        private final SampleDimension[] bands;
-
-        /**
-         * Creates a new buffered image wrapping the given raster.
-         */
-        @SuppressWarnings("UseOfObsoleteCollectionType")
-        Untiled(final ColorModel colors, final WritableRaster raster, final 
Hashtable<?,?> properties,
-                final int[] gridDimensions, final GridGeometry geometry, final 
SliceGeometry supplier, final SampleDimension[] bands)
-        {
-            super(colors, raster, false, properties);
-            this.gridDimensions = gridDimensions;
-            this.geometry       = geometry;
-            this.supplier       = supplier;
-            this.bands          = bands;
-        }
-
-        /**
-         * Returns the names of properties that this image can provide.
-         */
-        @Override
-        public String[] getPropertyNames() {
-            return ArraysExt.concatenate(super.getPropertyNames(), new 
String[] {
-                    XY_DIMENSIONS_KEY,
-                    GRID_GEOMETRY_KEY,
-                    SAMPLE_DIMENSIONS_KEY});
-        }
-
-        /**
-         * Returns the property associated to the given key.
-         * If the key is {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY},
-         * then the {@link GridGeometry} will be computed when first needed.
-         *
-         * @throws ImagingOpException if the property value cannot be computed.
-         */
-        @Override
-        public Object getProperty(final String key) {
-            switch (key) {
-                default: return super.getProperty(key);
-                case SAMPLE_DIMENSIONS_KEY: return bands.clone();
-                case XY_DIMENSIONS_KEY: return gridDimensions.clone();
-                case GRID_GEOMETRY_KEY: {
-                    synchronized (this) {
-                        if (geometry == null) {
-                            final SliceGeometry s = supplier;
-                            if (s != null) {
-                                supplier = null;                // Let GC do 
its work.
-                                geometry = s.apply(this);
-                            }
-                        }
-                        return geometry;
-                    }
-                }
-            }
-        }
-    }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
index 2280fdfc8a..ffc1441ef0 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java
@@ -33,7 +33,7 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.image.internal.shared.ImageUtilities;
 import org.apache.sis.image.internal.shared.TileOpExecutor;
 import org.apache.sis.image.internal.shared.ColorModelFactory;
-import org.apache.sis.image.internal.shared.ObservableImage;
+import org.apache.sis.image.internal.shared.WritableUntiledImage;
 
 
 /**
@@ -154,7 +154,7 @@ class BandSelectImage extends SourceAlignedImage {
                     properties.put(key, value);
                 }
             }
-            image = new ObservableImage(cm,
+            image = new WritableUntiledImage(cm,
                     bi.getRaster().createWritableChild(0, 0, bi.getWidth(), 
bi.getHeight(), 0, 0, bands),
                     bi.isAlphaPremultiplied(), properties);
         } else if (source instanceof WritableRenderedImage) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/WritableComputedImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/WritableComputedImage.java
index b27a9e6237..a689c83d00 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/WritableComputedImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/WritableComputedImage.java
@@ -22,7 +22,7 @@ import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
-import org.apache.sis.image.internal.shared.ObservableImage;
+import org.apache.sis.image.internal.shared.WritableUntiledImage;
 
 
 /**
@@ -104,7 +104,7 @@ abstract class WritableComputedImage extends ComputedImage {
      * @param  observer  the observer to notify.
      */
     public synchronized void addTileObserver(final TileObserver observer) {
-        observers = ObservableImage.addTileObserver(observers, observer);
+        observers = WritableUntiledImage.addTileObserver(observers, observer);
     }
 
     /**
@@ -115,7 +115,7 @@ abstract class WritableComputedImage extends ComputedImage {
      * @param  observer  the observer to stop notifying.
      */
     public synchronized void removeTileObserver(final TileObserver observer) {
-        observers = ObservableImage.removeTileObserver(observers, observer);
+        observers = WritableUntiledImage.removeTileObserver(observers, 
observer);
     }
 
     /**
@@ -129,7 +129,7 @@ abstract class WritableComputedImage extends ComputedImage {
     protected boolean markTileWritable(final int tileX, final int tileY, final 
boolean writing) {
         final boolean notify = super.markTileWritable(tileX, tileY, writing);
         if (notify && this instanceof WritableRenderedImage) {
-            ObservableImage.fireTileUpdate(observers, (WritableRenderedImage) 
this, tileX, tileY, writing);
+            WritableUntiledImage.fireTileUpdate(observers, 
(WritableRenderedImage) this, tileX, tileY, writing);
         }
         return notify;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
index 36b7481e2b..e52c1a543b 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
@@ -46,7 +46,7 @@ import static 
org.apache.sis.util.internal.shared.Numerics.COMPARISON_THRESHOLD;
 
 
 /**
- * Utility methods related to images and their color model or sample model.
+ * Utility methods related to images, their tiles, their color model and their 
sample model.
  * Those methods only fetch information, they do not create new rasters or 
sample/color models
  * (see {@code *Factory} classes for creating those objects).
  *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
index 59592dd328..230e8ad398 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
@@ -90,14 +90,14 @@ public final class RasterFactory {
             case DataBuffer.TYPE_BYTE:
             case DataBuffer.TYPE_USHORT: {
                 if (numComponents == 1 && 
ColorModelFactory.isStandardRange(dataType, minimum, maximum)) {
-                    return new ObservableImage(width, height, (dataType == 
DataBuffer.TYPE_BYTE)
+                    return new WritableUntiledImage(width, height, (dataType 
== DataBuffer.TYPE_BYTE)
                                 ? BufferedImage.TYPE_BYTE_GRAY : 
BufferedImage.TYPE_USHORT_GRAY);
                 }
                 break;
             }
         }
         final ColorModel cm = ColorModelFactory.createGrayScale(dataType, 
numComponents, visibleBand, minimum, maximum);
-        return new ObservableImage(cm, 
cm.createCompatibleWritableRaster(width, height), false, null);
+        return new WritableUntiledImage(cm, 
cm.createCompatibleWritableRaster(width, height), false, null);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
index 50e2967429..d2f87d2c12 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
@@ -51,7 +51,7 @@ public class WritableTiledImage extends TiledImage implements 
WritableRenderedIm
      * Values are counter of calls to {@link #getWritableTile(int, int)}.
      * All accesses to this map shall be synchronized on the map instance.
      */
-    private final Map<Point,Integer> writables;
+    private final Map<Point, Integer> writables;
 
     /**
      * Creates a new tiled image.
@@ -83,7 +83,7 @@ public class WritableTiledImage extends TiledImage implements 
WritableRenderedIm
      */
     @Override
     public synchronized void addTileObserver(final TileObserver observer) {
-        observers = ObservableImage.addTileObserver(observers, observer);
+        observers = WritableUntiledImage.addTileObserver(observers, observer);
     }
 
     /**
@@ -95,7 +95,7 @@ public class WritableTiledImage extends TiledImage implements 
WritableRenderedIm
      */
     @Override
     public synchronized void removeTileObserver(final TileObserver observer) {
-        observers = ObservableImage.removeTileObserver(observers, observer);
+        observers = WritableUntiledImage.removeTileObserver(observers, 
observer);
     }
 
     /**
@@ -115,7 +115,7 @@ public class WritableTiledImage extends TiledImage 
implements WritableRenderedIm
             count = writables.merge(key, 1, (old, one) -> old + 1);
         }
         if (count <= 1) {
-            ObservableImage.fireTileUpdate(observers, this, tileX, tileY, 
true);
+            WritableUntiledImage.fireTileUpdate(observers, this, tileX, tileY, 
true);
         }
         return tile;
     }
@@ -144,7 +144,7 @@ public class WritableTiledImage extends TiledImage 
implements WritableRenderedIm
             throw new 
IllegalArgumentException(Resources.format(Resources.Keys.TileNotWritable_2, 
tileX, tileY));
         }
         if (close) {
-            ObservableImage.fireTileUpdate(observers, this, tileX, tileY, 
false);
+            WritableUntiledImage.fireTileUpdate(observers, this, tileX, tileY, 
false);
         }
     }
 
@@ -157,7 +157,7 @@ public class WritableTiledImage extends TiledImage 
implements WritableRenderedIm
      */
     @Override
     public boolean isTileWritable(final int tileX, final int tileY) {
-        final Point key = new Point(tileX, tileY);
+        final var key = new Point(tileX, tileY);
         synchronized (writables) {
             return writables.containsKey(key);
         }
@@ -207,7 +207,7 @@ public class WritableTiledImage extends TiledImage 
implements WritableRenderedIm
         final Rectangle bounds = data.getBounds();
         ImageUtilities.clipBounds(this, bounds);
         if (!bounds.isEmpty()) {
-            final TileOpExecutor op = new TileOpExecutor(this, bounds) {
+            final var op = new TileOpExecutor(this, bounds) {
                 @Override protected void writeTo(final WritableRaster target) {
                     target.setRect(data);
                 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ObservableImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableUntiledImage.java
similarity index 66%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ObservableImage.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableUntiledImage.java
index 13192e2b3f..77a738ec84 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ObservableImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableUntiledImage.java
@@ -18,30 +18,46 @@ package org.apache.sis.image.internal.shared;
 
 import java.util.Arrays;
 import java.util.Hashtable;
+import java.util.function.Function;
 import java.awt.Point;
 import java.awt.image.TileObserver;
 import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
 import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
-import org.apache.sis.feature.internal.Resources;
+import java.awt.image.ImagingOpException;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.feature.internal.Resources;
+import static org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY;
 
 
 /**
- * A buffered image which can notify tile observers when tile are acquired for 
write operations.
- * Provides also helper methods for {@link WritableRenderedImage} 
implementations.
+ * The buffered image used by Apache <abbr>SIS</abbr> for untiled images.
+ * This class is preferred to {@link TiledImage} in the untiled case because
+ * Java2D has performance optimizations for instances of {@link BufferedImage}.
+ * This class is also preferred to instances of the exact {@link BufferedImage}
+ * class for the following reasons:
+ *
+ * <p>First, this class can notify tile observers when tiles are acquired for 
write operations.
+ * We cannot prevent {@link BufferedImage} to implement {@link 
WritableRenderedImage}, but we can
+ * increase the chances that Apache <abbr>SIS</abbr> is notified about pixel 
data modifications.
+ * For example, images given to {@link 
org.apache.sis.coverage.grid.GridCoverage2D} constructor
+ * are often used as sources of {@link org.apache.sis.image.ImageProcessor} 
operations,
+ * which listen to tile changes in order to flush the cache of invalidated 
tiles.</p>
+ *
+ * <p>Second, this class can compute the {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}
+ * property when first needed. We use this class even when the property value 
is known in advance
+ * because it has the desired side-effect of not letting {@link 
#getSubimage(int, int, int, int)}
+ * inherit that property.</p>
  *
- * <p>This class should be used in preference to {@link BufferedImage} when 
the image may be the
- * source of {@link org.apache.sis.image.ImageProcessor} operations. It is the 
case In particular
- * when this image is given to {@link 
org.apache.sis.coverage.grid.GridCoverage2D} constructor.
- * We cannot prevent {@link BufferedImage} to implement {@link 
WritableRenderedImage}, but we
- * can give a change to Apache SIS to be notified about modifications to pixel 
data.</p>
+ * <p>This class provides also static helper methods for {@link 
WritableRenderedImage} implementations.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public class ObservableImage extends BufferedImage {
+public final class WritableUntiledImage extends BufferedImage {
     /**
      * The observers, or {@code null} if none. This is a copy-on-write array:
      * values are never modified after construction (new arrays are created).
@@ -56,10 +72,20 @@ public class ObservableImage extends BufferedImage {
 
     /**
      * Number of times that the tile has been acquired for writing and not yet 
released.
-     * Write operations on this field should be in synchronized blocks.
+     * Write operations on this field should be done inside synchronized 
blocks.
      */
     private volatile int writeCount;
 
+    /**
+     * The value associated to the {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} key.
+     * May be a {@code Function<RenderedImage, GridGeometry} if the grid 
geometry is computed when
+     * first requested. This is {@code null} if there is no such property.
+     *
+     * This property is stored as a {@code WritableUntiledImage} field rather 
than a {@code Hashtable}
+     * entry for preventing inheritance by {@link #getSubimage(int, int, int, 
int)}.
+     */
+    private Object gridGeometry;
+
     /**
      * Creates an image of the specified type.
      *
@@ -67,20 +93,92 @@ public class ObservableImage extends BufferedImage {
      * @param height  image height.
      * @param type    one of {@code TYPE_*} constants.
      */
-    public ObservableImage(int width, int height, int type) {
+    public WritableUntiledImage(int width, int height, int type) {
         super(width, height, type);
     }
 
     /**
      * Creates an image using the specified raster.
      *
-     * @param colors  color model of the new image.
-     * @param raster  the singleton raster for the image data.
-     * @param isRasterPremultiplied   whether data in the raster has been 
premultiplied with alpha.
-     * @param properties  image properties as ({@code String}, {@code Object}) 
entries.
+     * @param colors         color model of the new image.
+     * @param raster         the singleton raster for the image data.
+     * @param premultiplied  whether data in the raster has been premultiplied 
with alpha.
+     * @param properties     image properties as ({@code String}, {@code 
Object}) entries.
+     */
+    @SuppressWarnings("UseOfObsoleteCollectionType")
+    public WritableUntiledImage(final ColorModel colors,
+                                final WritableRaster raster,
+                                final boolean premultiplied,
+                                final Hashtable<?,?> properties)
+    {
+        super(colors, raster, premultiplied, properties);
+    }
+
+    /**
+     * Sets the value associated to the {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} key.
+     * If the grid geometry is known in advance, it will be used. Otherwise 
the grid geometry will be
+     * computed when first requested using the given supplier.
+     *
+     * @param  ifKnown   the grid geometry, or {@code null} if no known in 
advance.
+     * @param  supplier  the function to execute when first needed, or {@code 
null} if none.
+     * @return {@code this} for method call chaining.
+     */
+    public WritableUntiledImage setGridGeometry(final GridGeometry ifKnown, 
final Function<RenderedImage, GridGeometry> supplier) {
+        gridGeometry = (ifKnown != null) ? ifKnown : supplier;
+        return this;
+    }
+
+    /**
+     * Returns the names of properties that this image can provide.
+     */
+    @Override
+    public String[] getPropertyNames() {
+        String[] names = super.getPropertyNames();  // May be null.
+        if (gridGeometry != null) {
+            if (names == null) {
+                names = new String[] {GRID_GEOMETRY_KEY};
+            } else {
+                names = ArraysExt.append(names, GRID_GEOMETRY_KEY);
+            }
+        }
+        return names;
+    }
+
+    /**
+     * Returns the property associated to the given key.
+     * If the key is {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY},
+     * then the {@link GridGeometry} will be computed when first needed.
+     *
+     * @param  name  name of the property to get.
+     * @return property value associated to the given name, or {@link 
#UndefinedProperty} if none.
+     * @throws ImagingOpException if the property value cannot be computed.
      */
-    public ObservableImage(ColorModel colors, WritableRaster raster, boolean 
isRasterPremultiplied, Hashtable<?,?> properties) {
-        super(colors, raster, isRasterPremultiplied, properties);
+    @Override
+    @SuppressWarnings("unchecked")
+    public Object getProperty(final String name) {
+        if (GRID_GEOMETRY_KEY.equals(name)) {
+            synchronized (this) {
+                if (gridGeometry != null) {
+                    if (gridGeometry instanceof GridGeometry) {
+                        return (GridGeometry) gridGeometry;
+                    }
+                    // `ClassCastException` should never occur here.
+                    return gridGeometry = ((Function<RenderedImage, 
GridGeometry>) gridGeometry).apply(this);
+                }
+            }
+        }
+        return super.getProperty(name);
+    }
+
+    /**
+     * Adds an observer to be notified when a tile is checked out for writing.
+     * If the observer is already present, it will receive multiple 
notifications.
+     *
+     * @param  observer  the observer to notify.
+     */
+    @Override
+    public synchronized void addTileObserver(final TileObserver observer) {
+        observers = addTileObserver(observers, observer);
     }
 
     /**
@@ -103,6 +201,18 @@ public class ObservableImage extends BufferedImage {
         return observers;
     }
 
+    /**
+     * Removes an observer from the list of observers notified when a tile is 
checked out for writing.
+     * If the observer was not registered, nothing happens. If the observer 
was registered for multiple
+     * notifications, it will now be registered for one fewer.
+     *
+     * @param  observer  the observer to stop notifying.
+     */
+    @Override
+    public synchronized void removeTileObserver(final TileObserver observer) {
+        observers = removeTileObserver(observers, observer);
+    }
+
     /**
      * Returns a new array with the specified observer removed from the 
specified array of observers.
      * If the observer was not registered, nothing happens and the given array 
is returned as-is.
@@ -123,6 +233,19 @@ public class ObservableImage extends BufferedImage {
         return observers;
     }
 
+    /**
+     * Notifies all listeners that the specified tile has been checked out for 
writing or has been released.
+     * The notifications are sent only if the given {@code count} is zero.
+     *
+     * @param count           value of {@link #writeCount} before increment or 
after decrement.
+     * @param willBeWritable  if {@code true}, the tile will be grabbed for 
writing; otherwise it is being released.
+     */
+    private void fireTileUpdate(final int count, final boolean willBeWritable) 
{
+        if (count == 0) {
+            fireTileUpdate(observers, this, 0, 0, willBeWritable);
+        }
+    }
+
     /**
      * Notifies all listeners that the specified tile has been checked out for 
writing or has been released.
      *
@@ -142,42 +265,6 @@ public class ObservableImage extends BufferedImage {
         }
     }
 
-    /**
-     * Adds an observer to be notified when a tile is checked out for writing.
-     * If the observer is already present, it will receive multiple 
notifications.
-     *
-     * @param  observer  the observer to notify.
-     */
-    @Override
-    public synchronized void addTileObserver(final TileObserver observer) {
-        observers = addTileObserver(observers, observer);
-    }
-
-    /**
-     * Removes an observer from the list of observers notified when a tile is 
checked out for writing.
-     * If the observer was not registered, nothing happens. If the observer 
was registered for multiple
-     * notifications, it will now be registered for one fewer.
-     *
-     * @param  observer  the observer to stop notifying.
-     */
-    @Override
-    public synchronized void removeTileObserver(final TileObserver observer) {
-        observers = removeTileObserver(observers, observer);
-    }
-
-    /**
-     * Notifies all listeners that the specified tile has been checked out for 
writing or has been released.
-     * The notifications are sent only if the given {@code count} is zero.
-     *
-     * @param count           value of {@link #writeCount} before increment or 
after decrement.
-     * @param willBeWritable  if {@code true}, the tile will be grabbed for 
writing; otherwise it is being released.
-     */
-    private void fireTileUpdate(final int count, final boolean willBeWritable) 
{
-        if (count == 0) {
-            fireTileUpdate(observers, this, 0, 0, willBeWritable);
-        }
-    }
-
     /**
      * Checks out a tile for writing. If the same tile is checked out many 
times
      * before to be released, only the first checkout is notified to listeners.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
index 0ef89b2d3d..ff53cdda2b 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/RasterReader.java
@@ -46,7 +46,7 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.image.internal.shared.ColorModelBuilder;
 import org.apache.sis.image.internal.shared.ColorModelFactory;
-import org.apache.sis.image.internal.shared.ObservableImage;
+import org.apache.sis.image.internal.shared.WritableUntiledImage;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.internal.shared.AffineTransform2D;
 import org.apache.sis.io.stream.ChannelDataInput;
@@ -336,7 +336,7 @@ public final class RasterReader extends RasterFormat {
             }
             cm = ColorModelFactory.createGrayScale(dataType, numBands, 
visibleBand, minimum, maximum);
         }
-        return new ObservableImage(cm, raster, false, null);
+        return new WritableUntiledImage(cm, raster, false, null);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
index 2edabbc94b..8a814d4bef 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
@@ -45,7 +45,7 @@ import org.apache.sis.storage.base.PRJDataStore;
 import org.apache.sis.storage.metadata.MetadataBuilder;
 import org.apache.sis.image.internal.shared.ColorModelFactory;
 import org.apache.sis.image.internal.shared.ColorModelBuilder;
-import org.apache.sis.image.internal.shared.ObservableImage;
+import org.apache.sis.image.internal.shared.WritableUntiledImage;
 import org.apache.sis.coverage.internal.shared.RangeArgument;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArraysExt;
@@ -464,7 +464,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
                 cm = ColorModelFactory.createGrayScale(data.getSampleModel(), 
VISIBLE_BAND, band.getSampleRange().orElse(null));
             }
         }
-        return new GridCoverage2D(domain, Arrays.asList(bands), new 
ObservableImage(cm, data, false, properties));
+        return new GridCoverage2D(domain, Arrays.asList(bands), new 
WritableUntiledImage(cm, data, false, properties));
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/image/internal/BufferedImages.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/image/internal/BufferedImages.java
index 7df7bfcf6d..06d42077e4 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/image/internal/BufferedImages.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/image/internal/BufferedImages.java
@@ -42,7 +42,7 @@ import org.apache.sis.image.PixelIterator;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.WritablePixelIterator;
 import org.apache.sis.image.internal.shared.ColorModelFactory;
-import org.apache.sis.image.internal.shared.ObservableImage;
+import org.apache.sis.image.internal.shared.WritableUntiledImage;
 import org.apache.sis.image.internal.shared.FillValues;
 import org.apache.sis.storage.util.TriFunction;
 import org.apache.sis.util.ArgumentChecks;
@@ -97,7 +97,7 @@ public class BufferedImages {
             if (cm == null) {
                 cm = ColorModelFactory.createGrayScale(dataType, nbBand, 0, 0, 
1);
             }
-            final BufferedImage resultImage = new ObservableImage(cm, raster, 
cm.isAlphaPremultiplied(), null);
+            final BufferedImage resultImage = new WritableUntiledImage(cm, 
raster, cm.isAlphaPremultiplied(), null);
             return resultImage;
         } else {
             //we need to create a new image
@@ -126,7 +126,7 @@ public class BufferedImages {
         //create a temporary fallback colormodel which will always work
         //extract grayscale min/max from sample dimension
         final ColorModel graycm = ColorModelFactory.createGrayScale(dataType, 
nbBand, 0, 0, 1);
-        return new ObservableImage(graycm, raster, false, null);
+        return new WritableUntiledImage(graycm, raster, false, null);
     }
 
     public static WritableRaster createRaster(int width, int height, int 
nbBand, int dataType, Point upperLeft) throws IllegalArgumentException{

Reply via email to