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 6ef7876 Initial implementation of CachedImage. The intent is to hold
the result of operations applied on other images. After completion, it will
replace ConvertedGridCoverage.
6ef7876 is described below
commit 6ef78764f2d78b7efed1ba687c44dc892bdc5e12
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Dec 30 19:31:00 2019 +0100
Initial implementation of CachedImage. The intent is to hold the result of
operations applied on other images.
After completion, it will replace ConvertedGridCoverage.
---
.../apache/sis/coverage/grid/GridCoverage2D.java | 4 +
.../java/org/apache/sis/image/PlanarImage.java | 68 +++++-
.../sis/internal/coverage/j2d/CachedImage.java | 270 +++++++++++++++++++++
.../sis/internal/coverage/j2d/TileCache.java | 141 +++++++++++
4 files changed, 478 insertions(+), 5 deletions(-)
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index 66fc23b..b53636c 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -539,6 +539,10 @@ public class GridCoverage2D extends GridCoverage {
* As per method contract, we shall set the (x,y) location to the
difference between requested region
* and actual region of the returned image. For example if the
user requested an image starting at
* (5,5) but the image to return starts at (1,1), then we need to
set its location to (-4,-4).
+ *
+ * Note: we could do a special case when the result has only one
tile and create a BufferedImage
+ * with Raster.createChild(…), but that would force us to invoke
RenderedImage.getTile(…) which
+ * may force data loading earlier than desired.
*/
final ReshapedImage r = new ReshapedImage(data, xmin, ymin, xmax,
ymax);
String error; assert (error = r.verify()) != null : error;
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index 0b329d0..c924ad4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -51,20 +51,26 @@ import
org.apache.sis.internal.coverage.j2d.ScaledColorSpace;
* We do not try to reproduce the full set of JAI functionalities here, but we
progressively
* reproduce some little bits of functionalities as they are needed by Apache
SIS.</p></div>
*
- * <p>This base class does not store any state and does not assume any tile
layout.
+ * <p>This base class does not store any state,
+ * but assumes that numbering of pixel coordinates and tile indices start at
zero.
* Subclasses need to implement at least the following methods:</p>
* <ul>
- * <li>{@link #getMinX()} — the minimum <var>x</var> coordinate
(inclusive) of the image.</li>
- * <li>{@link #getMinY()} — the minimum <var>y</var> coordinate
(inclusive) of the image.</li>
* <li>{@link #getWidth()} — the image width in pixels.</li>
* <li>{@link #getHeight()} — the image height in pixels.</li>
- * <li>{@link #getMinTileX()} — the minimum tile index in the
<var>x</var> direction.</li>
- * <li>{@link #getMinTileY()} — the minimum tile index in the
<var>y</var> direction.</li>
* <li>{@link #getTileWidth()} — the tile width in pixels.</li>
* <li>{@link #getTileHeight()} — the tile height in pixels.</li>
* <li>{@link #getTile(int,int)} — the tile at given tile indices.</li>
* </ul>
*
+ * <p>If pixel coordinates or tile indices do not start at zero,
+ * then subclasses shall also override the following methods:</p>
+ * <ul>
+ * <li>{@link #getMinX()} — the minimum <var>x</var> coordinate
(inclusive) of the image.</li>
+ * <li>{@link #getMinY()} — the minimum <var>y</var> coordinate
(inclusive) of the image.</li>
+ * <li>{@link #getMinTileX()} — the minimum tile index in the
<var>x</var> direction.</li>
+ * <li>{@link #getMinTileY()} — the minimum tile index in the
<var>y</var> direction.</li>
+ * </ul>
+ *
* Default implementations are provided for {@link #getNumXTiles()}, {@link
#getNumYTiles()},
* {@link #getTileGridXOffset()}, {@link #getTileGridYOffset()}, {@link
#getData()},
* {@link #getData(Rectangle)} and {@link #copyData(WritableRaster)}
@@ -133,6 +139,58 @@ public abstract class PlanarImage implements RenderedImage
{
}
/**
+ * Returns the minimum <var>x</var> coordinate (inclusive) of this image.
+ *
+ * <p>Default implementation returns zero.
+ * Subclasses shall override this method if the image starts at another
coordinate.</p>
+ *
+ * @return the minimum <var>x</var> coordinate (column) of this image.
+ */
+ @Override
+ public int getMinX() {
+ return 0;
+ }
+
+ /**
+ * Returns the minimum <var>y</var> coordinate (inclusive) of this image.
+ *
+ * <p>The default implementation returns zero.
+ * Subclasses shall override this method if the image starts at another
coordinate.</p>
+ *
+ * @return the minimum <var>y</var> coordinate (row) of this image.
+ */
+ @Override
+ public int getMinY() {
+ return 0;
+ }
+
+ /**
+ * Returns the minimum tile index in the <var>x</var> direction.
+ *
+ * <p>The default implementation returns zero.
+ * Subclasses shall override this method if the tile grid starts at
another index.</p>
+ *
+ * @return the minimum tile index in the <var>x</var> direction.
+ */
+ @Override
+ public int getMinTileX() {
+ return 0;
+ }
+
+ /**
+ * Returns the minimum tile index in the <var>y</var> direction.
+ *
+ * <p>The default implementation returns zero.
+ * Subclasses shall override this method if the tile grid starts at
another index.</p>
+ *
+ * @return the minimum tile index in the <var>y</var> direction.
+ */
+ @Override
+ public int getMinTileY() {
+ return 0;
+ }
+
+ /**
* Returns the number of tiles in the <var>x</var> direction.
*
* <p>The default implementation computes this value from {@link
#getWidth()} and {@link #getTileWidth()}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/CachedImage.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/CachedImage.java
new file mode 100644
index 0000000..f518429
--- /dev/null
+++
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/CachedImage.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.util.Set;
+import java.util.HashSet;
+import java.awt.Point;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.awt.image.ImagingOpException;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import java.lang.ref.WeakReference;
+import org.apache.sis.internal.system.ReferenceQueueConsumer;
+import org.apache.sis.image.PlanarImage;
+import org.apache.sis.util.collection.Cache;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Disposable;
+
+
+/**
+ * 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.
+ *
+ * <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
+ * @module
+ */
+public abstract class CachedImage extends PlanarImage {
+ /**
+ * Weak reference to the enclosing image together with necessary
information for releasing resources
+ * when image is disposed. This class shall not contain any strong
reference to the enclosing image.
+ */
+ // MUST be static
+ private static final class Cleaner extends WeakReference<CachedImage>
implements Disposable {
+ /**
+ * Indices of all cached tiles. Used for removing tiles from the cache
when the image is disposed.
+ * All accesses to this collection must be synchronized. This field
has to be declared here because
+ * {@link Cleaner} is not allowed to keep a strong reference to the
enclosing {@link CachedImage}.
+ */
+ private final Set<TileCache.Key> cachedTiles;
+
+ /**
+ * Creates a new weak reference to the given image.
+ */
+ Cleaner(final CachedImage image) {
+ super(image, ReferenceQueueConsumer.QUEUE);
+ cachedTiles = new HashSet<>();
+ }
+
+ /**
+ * Remember 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) {
+ synchronized (cachedTiles) {
+ cachedTiles.add(key);
+ }
+ }
+
+ /**
+ * Invoked when the enclosing image has been garbage-collected. This
method removes all cached tiles
+ * that were owned by the enclosing image. This method should not
perform other cleaning work than
+ * removing cached tiles because it is not guaranteed to be invoked if
{@link TileCache#GLOBAL}
+ * does not contain any tile for the enclosing image.
+ */
+ @Override
+ public void dispose() {
+ synchronized (cachedTiles) {
+ cachedTiles.forEach(TileCache.Key::dispose);
+ cachedTiles.clear();
+ }
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private final Cleaner reference;
+
+ /**
+ * The sample model shared by all tiles in this image.
+ * The {@linkplain SampleModel#getWidth() sample model width}
+ * determines this {@linkplain #getTileWidth() image tile width},
+ * and the {@linkplain SampleModel#getHeight() sample model height}
+ * determines this {@linkplain #getTileHeight() image tile height}.
+ *
+ * <div class="note"><b>Design note:</b>
+ * {@code CachedImage} requires the sample model to have exactly the
desired tile size
+ * otherwise tiles created by {@link #createTile(int, int)} will consume
more memory
+ * than needed.</div>
+ */
+ protected final SampleModel sampleModel;
+
+ /**
+ * Creates an initially empty image with the given sample model.
+ * The tile size will be the width and height of the given sample model.
+ *
+ * @param sampleModel the sample model shared by all tiles in this image.
+ */
+ protected CachedImage(final SampleModel sampleModel) {
+ ArgumentChecks.ensureNonNull("sampleModel", sampleModel);
+ reference = new Cleaner(this);
+ this.sampleModel = sampleModel;
+ }
+
+ /**
+ * Creates an initially empty image with a sample model derived from the
given image.
+ * This constructor sets {@link #sampleModel} to a model compatible with
the one used
+ * by the given image, but with {@linkplain SampleModel#getWidth() width}
and
+ * {@linkplain SampleModel#getHeight() height} matching exactly the size
of the tiles.
+ *
+ * <p>This constructor does <strong>not</strong> inherit other image
properties.
+ * In particular pixel coordinates and tile indices in this image start at
(0,0)
+ * unless subclass override {@link #getMinX()}, {@link #getMinY()}, {@link
#getMinTileX()}
+ * and {@link #getMinTileY()}.</p>
+ *
+ * @param image the image from which to get tile size.
+ */
+ protected CachedImage(final RenderedImage image) {
+ ArgumentChecks.ensureNonNull("image", image);
+ reference = new Cleaner(this);
+ sampleModel = adapt(image.getSampleModel(), image.getTileWidth(),
image.getTileHeight());
+ }
+
+ /**
+ * Returns a sample model compatible with the given one, but with the
specified width and height.
+ * This method checks if the given sample model can be used as-is and
create a new one only if needed.
+ * This restriction about sample model size matching tile size is for
reducing the amount of memory
+ * consumed by {@link #createTile(int, int)}.
+ */
+ private static SampleModel adapt(SampleModel sampleModel, final int width,
final int height) {
+ if (sampleModel.getWidth() != width || sampleModel.getHeight() !=
height) {
+ sampleModel = sampleModel.createCompatibleSampleModel(width,
height);
+ }
+ return sampleModel;
+ }
+
+ /**
+ * Returns the width of tiles in this image. The default implementation
returns {@link SampleModel#getWidth()}.
+ *
+ * <div class="note"><b>Note:</b>
+ * a raster can have a smaller width than its sample model, for example
when a raster is a view over a subregion
+ * of another raster. But this is not recommended in the particular case
of this {@code CachedImage} class,
+ * because it would cause {@link #createTile(int, int)} to consume more
memory than necessary.</div>
+ *
+ * @return the width of this image in pixels.
+ */
+ @Override
+ public int getTileWidth() {
+ return sampleModel.getWidth();
+ }
+
+ /**
+ * Returns the height of tiles in this image. The default implementation
returns {@link SampleModel#getHeight()}.
+ *
+ * <div class="note"><b>Note:</b>
+ * a raster can have a smaller height than its sample model, for example
when a raster is a view over a subregion
+ * of another raster. But this is not recommended in the particular case
of this {@code CachedImage} class,
+ * because it would cause {@link #createTile(int, int)} to consume more
memory than necessary.</div>
+ *
+ * @return the height of this image in pixels.
+ */
+ @Override
+ public int getTileHeight() {
+ return sampleModel.getHeight();
+ }
+
+ /**
+ * Returns a tile of this image, computing it when needed.
+ * 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>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>
+ * <li>Otherwise this method computes the tile and caches the result
before to return it.
+ * If an error occurred, an {@link ImagingOpException} is
thrown.</li>
+ * </ol>
+ *
+ * @param tileX the column index of the tile to get.
+ * @param tileY the row index of the tile to get.
+ * @return the tile at the given index (never null).
+ * @throws IndexOutOfBoundsException if a given tile index is out of
bounds.
+ * @throws ImagingOpException if an error occurred while computing the
image.
+ */
+ @Override
+ public final Raster getTile(final int tileX, final int tileY) {
+ final TileCache.Key key = new TileCache.Key(reference, tileX, tileY);
+ final Cache<TileCache.Key,Raster> cache = TileCache.GLOBAL;
+ Raster tile = cache.peek(key);
+ if (tile == null) {
+ final Cache.Handler<Raster> handler = cache.lock(key);
+ try {
+ tile = handler.peek();
+ if (tile == null) {
+ tile = computeTile(tileX, tileY);
+ if (tile == null) {
+ throw new ImagingOpException("No data"); // TODO
+ }
+ }
+ } finally {
+ handler.putAndUnlock(tile); // Must be invoked even if an
exception occurred.
+ }
+ }
+ return tile;
+ }
+
+ /**
+ * Invoked when a tile need to be computed. This method is invoked by
{@link #getTile(int, int)}
+ * when the requested tile is not in the cache. The returned tile will be
automatically cached.
+ *
+ * @param tileX the column index of the tile to compute.
+ * @param tileY the row index of the tile to compute.
+ * @return computed tile for the given indices (can not be null).
+ */
+ protected abstract Raster computeTile(int tileX, int tileY);
+
+ /**
+ * Creates an initially empty tile at the given tile grid position.
+ * This is a helper method for {@link #computeTile(int, int)}
implementations.
+ *
+ * @param tileX the column index of the tile to create.
+ * @param tileY the row index of the tile to create.
+ * @return initially empty tile for the given indices (can not be null).
+ */
+ protected WritableRaster createTile(final int tileX, final int tileY) {
+ // A temporary `int` overflow may occur before the final addition.
+ final int x = Math.toIntExact((((long) tileX) - getMinTileX()) *
getTileWidth() + getMinX());
+ final int y = Math.toIntExact((((long) tileY) - getMinTileY()) *
getTileHeight() + getMinY());
+ return WritableRaster.createWritableRaster(getSampleModel(), new
Point(x,y));
+ }
+
+ /**
+ * Advises this image that its tiles will no longer be requested.
+ * This method removes all tiles from the cache.
+ * This image should not be used anymore after this method call.
+ *
+ * <p><b>Note:</b> keep in mind that this image may be referenced as a
source of other images.
+ * In case of doubt, it may be safer to rely on the garbage collector
instead than invoking this method.</p>
+ */
+ public void dispose() {
+ reference.dispose();
+ }
+}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileCache.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileCache.java
new file mode 100644
index 0000000..3da725a
--- /dev/null
+++
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileCache.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+import java.lang.ref.Reference;
+import org.apache.sis.util.collection.Cache;
+
+
+/**
+ * A cache of tiles computed by {@link CachedImage}. A common cache is shared
by all images.
+ * Tiles are kept by strong references until a memory usage limit is reached,
in which case
+ * the references of oldest tiles become soft references.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class TileCache extends Cache<TileCache.Key, Raster> {
+ /**
+ * The instance shared by all image operations. Current version does not
allow to specify
+ * a custom cache for some images, but a future version may allow that.
+ */
+ static final TileCache GLOBAL = new TileCache();
+
+ /**
+ * Creates a new tile cache.
+ */
+ private TileCache() {
+ /*
+ * We put an arbitrary limit of 25% of available memory. If more tiles
are created,
+ * some strong references will become soft references. Since strong
references may
+ * be kept by the JVM, the amount of memory actually used may be
greater than this
+ * limit.
+ */
+ super(100, Runtime.getRuntime().maxMemory() / 4, true);
+ }
+
+ /**
+ * Returns an estimation of the memory consumption of the given tile.
+ *
+ * @param tile the tile for which to estimate memory usage.
+ * @return memory used by the given tile, in bytes.
+ */
+ @Override
+ protected int cost(final Raster tile) {
+ long numBits = ((long) tile.getWidth()) * tile.getHeight() *
tile.getNumBands();
+ final DataBuffer buffer = tile.getDataBuffer();
+ if (buffer != null) try {
+ numBits *= DataBuffer.getDataTypeSize(buffer.getDataType());
+ } catch (IllegalArgumentException e) {
+ numBits *= Integer.SIZE; // Conservatively assume
32 bits values.
+ }
+ return (int) Math.min(Integer.MIN_VALUE, numBits / Byte.SIZE);
+ }
+
+ /**
+ * A compound key identifying a tile of a {@link CachedImage}.
+ */
+ static final class Key {
+ /**
+ * The image which own the tile as a weak reference. All {@code
TileCache.Key} instances
+ * for the same image will share the same reference. Consequently it
is okay to compare
+ * {@code image} fields directly instead of {@code image.get()}.
+ */
+ private final Reference<CachedImage> image;
+
+ /**
+ * Index of the tile owned by the image.
+ */
+ private final int tileX, tileY;
+
+ /**
+ * Creates a new key identifying a tile or a cached image.
+ *
+ * @param image the image which own the tile.
+ * @param tileX the column index of the cached tile.
+ * @param tileY the row index of the cached tile.
+ */
+ Key(final Reference<CachedImage> image, final int tileX, final int
tileY) {
+ this.image = image;
+ this.tileX = tileX;
+ this.tileY = tileY;
+ }
+
+ /**
+ * Removes the raster associated to this key. This method is invoked
+ * for all tiles in an image being disposed.
+ */
+ final void dispose() {
+ GLOBAL.remove(this);
+ }
+
+ /**
+ * Returns a hash code value for this key. Note that this is okay to
use {@link #image} directly
+ * in hash code computation instead of {@link Reference#get()} because
we maintain a one-to-one
+ * relationship between {@link CachedImage} and its {@link Reference}.
+ */
+ @Override
+ public int hashCode() {
+ return Long.hashCode(System.identityHashCode(image) + tileX +
65563L * tileY);
+ }
+
+ /**
+ * Compares this key with the given object for equality. See {@link
#hashCode()} for a note about
+ * direct comparison of {@link #image} references.
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj instanceof TileCache.Key) {
+ final TileCache.Key k = (TileCache.Key) obj;
+ return image == k.image && tileX == k.tileX && tileY ==
k.tileY;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a string representation of this key for debugging purposes.
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + '[' + tileX + ", " + tileY +
']';
+ }
+ }
+}