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();
+ }
+}