You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/10/20 16:57:08 UTC

[sis] branch geoapi-4.0 updated: Move the provider of empty tiles in a location where it can be shared with other operations.

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 da6ade3  Move the provider of empty tiles in a location where it can be shared with other operations.
da6ade3 is described below

commit da6ade3d07fa0c82b90167821c3ba57d5eb27d67
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Oct 20 17:47:09 2021 +0200

    Move the provider of empty tiles in a location where it can be shared with other operations.
---
 .../java/org/apache/sis/image/PrefetchedImage.java | 107 +-------
 .../sis/internal/coverage/j2d/TilePlaceholder.java | 302 +++++++++++++++++++++
 2 files changed, 310 insertions(+), 99 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
index b5ac0ac..fd15bb9 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
@@ -16,25 +16,20 @@
  */
 package org.apache.sis.image;
 
-import java.util.Arrays;
 import java.util.Vector;
 import java.awt.Point;
 import java.awt.Rectangle;
-import java.awt.color.ColorSpace;
 import java.awt.image.ColorModel;
-import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
 import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
-import java.awt.image.WritableRaster;
 import java.awt.image.RasterFormatException;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.TileErrorHandler;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
-import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.internal.coverage.j2d.TilePlaceholder;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.Workaround;
 
 
 /**
@@ -51,24 +46,6 @@ import org.apache.sis.util.Workaround;
  */
 final class PrefetchedImage extends PlanarImage implements TileErrorHandler.Executor {
     /**
-     * Identifies workaround for a JDK bug: call to {@code Graphics2D.drawRenderedImage(…)}
-     * fails if the image contains more than one tile (or a single tile not located at 0,0)
-     * and the tiles are not instances of {@link WritableRaster} (i.e. are instances of the
-     * read-only {@link Raster} parent class). The exception thrown is:
-     *
-     * {@preformat text
-     *   Exception in thread "main" java.awt.image.RasterFormatException: (parentX + width) is outside raster
-     *       at java.desktop/java.awt.image.WritableRaster.createWritableChild(WritableRaster.java:228)
-     *       at java.desktop/sun.java2d.SunGraphics2D.drawTranslatedRenderedImage(SunGraphics2D.java:2852)
-     *       at java.desktop/sun.java2d.SunGraphics2D.drawRenderedImage(SunGraphics2D.java:2711)
-     * }
-     *
-     * @see <a href="https://bugs.openjdk.java.net/browse/JDK-8275345">JDK-8275345</a>
-     */
-    @Workaround(library="JDK", version="17")
-    private static final boolean PENDING_JDK_FIX = false;
-
-    /**
      * The source image from which to prefetch tiles.
      */
     final RenderedImage source;
@@ -99,7 +76,7 @@ final class PrefetchedImage extends PlanarImage implements TileErrorHandler.Exec
      *
      * @see #createPlaceholder(int, int)
      */
-    private DataBuffer placeholderPixels;
+    private volatile TilePlaceholder placeholderPixels;
 
     /**
      * Non-null if errors should be handled during {@link #getTile(int, int)} execution for tiles outside
@@ -304,80 +281,12 @@ final class PrefetchedImage extends PlanarImage implements TileErrorHandler.Exec
      * @return placeholder for the tile at given indices.
      */
     private Raster createPlaceholder(final int tileX, final int tileY) {
-        final SampleModel model = getSampleModel();
-        final Point location = new Point(ImageUtilities.tileToPixelX(source, tileX),
-                                         ImageUtilities.tileToPixelY(source, tileY));
-        if (placeholderPixels != null) {
-            if (!PENDING_JDK_FIX) {
-                return Raster.createWritableRaster(model, placeholderPixels, location);
-            }
-            // Reuse same `DataBuffer` with only a different location.
-            return Raster.createRaster(model, placeholderPixels, location);
-        }
-        final double[] samples = new double[model.getNumBands()];
-        if (ImageUtilities.isIntegerType(model)) {
-            final boolean isUnsigned = ImageUtilities.isUnsignedType(model);
-            for (int i=0; i<samples.length; i++) {
-                int size = model.getSampleSize(i);
-                if (!isUnsigned) size--;
-                samples[i] = Numerics.bitmask(size) - 1;
-            }
-        } else {
-            final ColorSpace cs;
-            final ColorModel cm = getColorModel();
-            if (cm != null && (cs = cm.getColorSpace()) != null) {
-                for (int i = Math.min(cs.getNumComponents(), samples.length); --i >=0;) {
-                    samples[i] = cs.getMaxValue(i);
-                }
-            } else {
-                Arrays.fill(samples, 1);
-            }
-        }
-        /*
-         * Draw borders around the tile as dotted lines. The left border will have (usually) white pixels
-         * at even coordinates relative to upper-left corner, while right border will have same pixels at
-         * odd coordinates. The same pattern applies to top and bottom borders.
-         */
-        final WritableRaster tile = WritableRaster.createWritableRaster(model, model.createDataBuffer(), location);
-        final int width  = tile.getWidth();
-        final int height = tile.getHeight();
-        final int xmin   = tile.getMinX();
-        final int ymin   = tile.getMinY();
-        final int xmax   = width  + xmin - 1;
-        final int ymax   = height + ymin - 1;
-        int x = xmin;
-        while (x < xmax) {
-            tile.setPixel(x++, ymin, samples);
-            tile.setPixel(x++, ymax, samples);
-        }
-        int y = ymin;
-        while (y < ymax) {
-            tile.setPixel(xmin, y++, samples);
-            tile.setPixel(xmax, y++, samples);
-        }
-        if (x == xmax) tile.setPixel(xmax, ymin, samples);
-        if (y == ymax) tile.setPixel(xmin, ymax, samples);
-        /*
-         * Add a cross (X) inside the tile.
-         */
-        if (width >= height) {
-            final double step = height / (double) width;
-            for (int i=0; i<width; i++) {
-                x = xmin + i;
-                y = (int) (i*step);
-                tile.setPixel(x, ymin + y, samples);
-                tile.setPixel(x, ymax - y, samples);
-            }
-        } else {
-            final double step = width / (double) height;
-            for (int i=0; i<height; i++) {
-                y = ymin + i;
-                x = (int) (i*step);
-                tile.setPixel(xmin + x, y, samples);
-                tile.setPixel(xmax - x, y, samples);
-            }
+        TilePlaceholder p = placeholderPixels;
+        if (p == null) {
+            // Not a problem if invoked concurrently in two threads.
+            placeholderPixels = p = TilePlaceholder.withCross(source);
         }
-        placeholderPixels = tile.getDataBuffer();
-        return tile;
+        return p.create(new Point(ImageUtilities.tileToPixelX(source, tileX),
+                                  ImageUtilities.tileToPixelY(source, tileY)));
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TilePlaceholder.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TilePlaceholder.java
new file mode 100644
index 0000000..efd048d
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TilePlaceholder.java
@@ -0,0 +1,302 @@
+/*
+ * 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.Arrays;
+import java.awt.Point;
+import java.awt.color.ColorSpace;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.awt.image.RenderedImage;
+import java.lang.ref.WeakReference;
+import org.apache.sis.util.Disposable;
+import org.apache.sis.util.Workaround;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.collection.WeakHashSet;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.internal.system.ReferenceQueueConsumer;
+
+
+/**
+ * Source of read-only empty tiles to use as placeholder for tiles that were not computed.
+ * This class reduces memory usage by sharing the same data buffer for all tiles.
+ * Subclasses can optionally draw some symbols in the tile, for example to signal an error.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+public class TilePlaceholder {
+    /**
+     * Identifies workaround for a JDK bug: call to {@code Graphics2D.drawRenderedImage(…)}
+     * fails if the image contains more than one tile (or a single tile not located at 0,0)
+     * and the tiles are not instances of {@link WritableRaster} (i.e. are instances of the
+     * read-only {@link Raster} parent class). The exception thrown is:
+     *
+     * {@preformat text
+     *   Exception in thread "main" java.awt.image.RasterFormatException: (parentX + width) is outside raster
+     *       at java.desktop/java.awt.image.WritableRaster.createWritableChild(WritableRaster.java:228)
+     *       at java.desktop/sun.java2d.SunGraphics2D.drawTranslatedRenderedImage(SunGraphics2D.java:2852)
+     *       at java.desktop/sun.java2d.SunGraphics2D.drawRenderedImage(SunGraphics2D.java:2711)
+     * }
+     *
+     * @see <a href="https://bugs.openjdk.java.net/browse/JDK-8275345">JDK-8275345</a>
+     */
+    @Workaround(library="JDK", version="17")
+    private static final boolean PENDING_JDK_FIX = false;
+
+    /**
+     * Cache of empty tiles for different sample models.
+     */
+    private static final WeakHashSet<TilePlaceholder> CACHE = new WeakHashSet<>(TilePlaceholder.class);
+
+    /**
+     * The sample model of the empty tiles to create.
+     */
+    protected final SampleModel model;
+
+    /**
+     * The data buffers for empty tiles. The same data buffer is shared by all {@link Raster}
+     * instances using the same sample model. Data may contain some drawing such as a cross (X).
+     */
+    private BufferRef reference;
+
+    /**
+     * Weak reference to a data buffer. The enclosing {@link TilePlaceholder} is protected
+     * from garbage collection as long as the weak reference is live.
+     */
+    private static final class BufferRef extends WeakReference<DataBuffer> implements Disposable {
+        /**
+         * Prevents the enclosing class to be garbage-collected too early by the {@link #CACHE}.
+         * If the {@link DataBuffer} is still referenced by some {@link Raster}, then we need the
+         * {@link TilePlaceholder} to continue to exist in the {@link #CACHE} in order to be able
+         * to reuse that {@code DataBuffer}.
+         */
+        private TilePlaceholder owner;
+
+        /**
+         * Creates a new reference to the given buffer.
+         */
+        BufferRef(TilePlaceholder owner, final DataBuffer buffer) {
+            super(buffer, ReferenceQueueConsumer.QUEUE);
+            this.owner = owner;
+        }
+
+        /**
+         * Allows the enclosing {@link TilePlaceholder} to be garbage-collected.
+         */
+        @Override
+        public void dispose() {
+            owner = null;
+        }
+    }
+
+    /**
+     * Creates a initially empty set of empty tiles.
+     *
+     * @param  model  sample model of the empty tiles.
+     */
+    protected TilePlaceholder(final SampleModel model) {
+        ArgumentChecks.ensureNonNull("model", model);
+        this.model = model;
+    }
+
+    /**
+     * Returns a provider of empty tiles for the given sample model.
+     * All pixel values will be zero in all bands.
+     *
+     * @param  model  sample model of the empty tiles.
+     * @return provider of fully empty tiles.
+     */
+    public static TilePlaceholder empty(final SampleModel model) {
+        return CACHE.unique(new TilePlaceholder(model));
+    }
+
+    /**
+     * Returns a source of "empty" tiles with a white border and a white cross.
+     *
+     * @param  image  sample model and color model of the tiles to create.
+     * @return a source of "empty" tiles with white borders and crosses.
+     */
+    public static TilePlaceholder withCross(final RenderedImage image) {
+        return CACHE.unique(new WithCross(image));
+    }
+
+    /**
+     * Creates a tile to use as a placeholder when a tile can not be computed.
+     *
+     * @param  location  minimum x and y coordinates of the tile raster.
+     * @return placeholder for a tile at the given location.
+     */
+    public final Raster create(final Point location) {
+        DataBuffer buffer;
+        synchronized (this) {
+            if (reference == null || (buffer = reference.get()) == null) {
+                buffer = model.createDataBuffer();
+                reference = new BufferRef(this, buffer);      // Cache even if only partially drawn.
+                if (getClass() != TilePlaceholder.class) {
+                    final WritableRaster tile = Raster.createWritableRaster(model, buffer, location);
+                    draw(tile);
+                    return tile;
+                }
+                // Else prefer read-only tile (created below) if we do not need to draw anything.
+            }
+        }
+        if (!PENDING_JDK_FIX) {
+            return Raster.createWritableRaster(model, buffer, location);
+        }
+        // Reuse same `DataBuffer` with only a different location.
+        return Raster.createRaster(model, buffer, location);
+    }
+
+    /**
+     * Invoked when a new empty tile is created. Subclasses can override this method
+     * for drawing some visual indication that the tile is missing.
+     * The default implementation does nothing.
+     *
+     * @param  tile  the tile where to draw.
+     */
+    protected void draw(final WritableRaster tile) {
+    }
+
+
+
+
+    /**
+     * A provider of tile placeholder with a white border and white cross in the tile.
+     */
+    private static final class WithCross extends TilePlaceholder {
+        /**
+         * The sample values to use for the border and the cross.
+         */
+        private final double[] samples;
+
+        /**
+         * Creates a new provider for the sample model and color model of given image.
+         *
+         * @param  image  sample model and color model of the tiles to create.
+         */
+        WithCross(final RenderedImage image) {
+            super(image.getSampleModel());
+            samples = new double[model.getNumBands()];
+            if (ImageUtilities.isIntegerType(model)) {
+                final boolean isUnsigned = ImageUtilities.isUnsignedType(model);
+                for (int i=0; i<samples.length; i++) {
+                    int size = model.getSampleSize(i);
+                    if (!isUnsigned) size--;
+                    samples[i] = Numerics.bitmask(size) - 1;
+                }
+            } else {
+                final ColorSpace cs;
+                final ColorModel cm = image.getColorModel();
+                if (cm != null && (cs = cm.getColorSpace()) != null) {
+                    for (int i = Math.min(cs.getNumComponents(), samples.length); --i >=0;) {
+                        samples[i] = cs.getMaxValue(i);
+                    }
+                } else {
+                    Arrays.fill(samples, 1);
+                }
+            }
+        }
+
+        /**
+         * Draw borders around the tile as dotted lines. The left border will have (usually) white pixels
+         * at even coordinates relative to upper-left corner, while right border will have same pixels at
+         * odd coordinates. The same pattern applies to top and bottom borders.
+         */
+        @Override
+        protected void draw(final WritableRaster tile) {
+            final int width  = tile.getWidth();
+            final int height = tile.getHeight();
+            final int xmin   = tile.getMinX();
+            final int ymin   = tile.getMinY();
+            final int xmax   = width  + xmin - 1;
+            final int ymax   = height + ymin - 1;
+            int x = xmin;
+            while (x < xmax) {
+                tile.setPixel(x++, ymin, samples);
+                tile.setPixel(x++, ymax, samples);
+            }
+            int y = ymin;
+            while (y < ymax) {
+                tile.setPixel(xmin, y++, samples);
+                tile.setPixel(xmax, y++, samples);
+            }
+            if (x == xmax) tile.setPixel(xmax, ymin, samples);
+            if (y == ymax) tile.setPixel(xmin, ymax, samples);
+            /*
+             * Add a cross (X) inside the tile.
+             */
+            if (width >= height) {
+                final double step = height / (double) width;
+                for (int i=0; i<width; i++) {
+                    x = xmin + i;
+                    y = (int) (i*step);
+                    tile.setPixel(x, ymin + y, samples);
+                    tile.setPixel(x, ymax - y, samples);
+                }
+            } else {
+                final double step = width / (double) height;
+                for (int i=0; i<height; i++) {
+                    y = ymin + i;
+                    x = (int) (i*step);
+                    tile.setPixel(xmin + x, y, samples);
+                    tile.setPixel(xmax - x, y, samples);
+                }
+            }
+        }
+
+        /**
+         * Compares this object with given object for equality.
+         */
+        @Override
+        public boolean equals(final Object obj) {
+            return super.equals(obj) && Arrays.equals(((WithCross) obj).samples, samples);
+        }
+
+        /**
+         * Returns a hash code value for this provider of tile placeholders.
+         */
+        @Override
+        public int hashCode() {
+            return super.hashCode() + Arrays.hashCode(samples);
+        }
+    }
+
+    /**
+     * Compares this object with given object for equality.
+     *
+     * @param  obj  the other object to compare with this object.
+     * @return {@code true} if the two objects will create equivalent empty tiles.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        return (obj != null) && (obj.getClass() == getClass()) && model.equals(((TilePlaceholder) obj).model);
+    }
+
+    /**
+     * Returns a hash code value for this provider of tile placeholders.
+     */
+    @Override
+    public int hashCode() {
+        return getClass().hashCode() - model.hashCode();
+    }
+}