sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/04: Render image in background for avoiding to block the JavaFX thread.
Date Mon, 02 Mar 2020 19:33:15 GMT
This is an automated email from the ASF dual-hosted git repository.

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

commit 8aa76e323f05802b643b5f533f8adae1346f8f10
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Mar 2 09:31:20 2020 +0100

    Render image in background for avoiding to block the JavaFX thread.
---
 .../apache/sis/gui/coverage/CoverageControls.java  |   2 +-
 .../org/apache/sis/gui/coverage/CoverageView.java  | 402 ++++++++++++++++-----
 2 files changed, 315 insertions(+), 89 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index c6aea60..8678e28 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -67,7 +67,7 @@ final class CoverageControls extends Controls {
         {   // Block for making variables locale to this scope.
             final GridPane gp = createControlGrid(
                 label(vocabulary, Vocabulary.Keys.Background, createBackgroundButton(background)),
-                label(vocabulary, Vocabulary.Keys.ValueRange, RangeType.createButton(view::onRangeTypeChanged))
+                label(vocabulary, Vocabulary.Keys.ValueRange, RangeType.createButton((p,o,n)
-> view.setRangeType(n)))
             );
             final Label label = new Label(vocabulary.getLabel(Vocabulary.Keys.Image));
             label.setPadding(CAPTION_MARGIN);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
index 76f629f..1b46e1d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
@@ -18,8 +18,10 @@ package org.apache.sis.gui.coverage;
 
 import java.util.Locale;
 import java.util.EnumMap;
+import java.util.Objects;
 import java.nio.IntBuffer;
 import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
 import java.awt.geom.AffineTransform;
 import java.awt.image.BufferedImage;
 import java.awt.image.DataBufferInt;
@@ -38,12 +40,15 @@ import javafx.scene.layout.BackgroundFill;
 import javafx.beans.value.ObservableValue;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.concurrent.Task;
 import javafx.util.Callback;
 import org.opengis.referencing.datum.PixelInCell;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.ImageRenderings;
 import org.apache.sis.internal.map.PlanarCanvas;
 import org.apache.sis.internal.util.Numerics;
@@ -85,10 +90,19 @@ final class CoverageView extends PlanarCanvas {
 
     /**
      * Different ways to represent the data. The {@link #data} field shall be one value from
this map.
+     *
+     * @see #setImage(RangeType, RenderedImage)
      */
     private final EnumMap<RangeType,RenderedImage> dataAlternatives;
 
     /**
+     * Key of the currently selected alternative in {@link #dataAlternatives} map.
+     *
+     * @see #setImage(RangeType, RenderedImage)
+     */
+    private RangeType currentDataAlternative;
+
+    /**
      * The data to shown, or {@code null} if not yet specified. This image may be tiled,
      * and fetching tiles may require computations to be performed in background thread.
      * The size of this image is not necessarily {@link #buffer} or {@link #image} size.
@@ -105,6 +119,20 @@ final class CoverageView extends PlanarCanvas {
     private BufferedImage buffer;
 
     /**
+     * A temporary buffer where to draw the {@link RenderedImage} in a background thread.
+     * We use this double-buffering when the {@link #buffer} is already wrapped by JavaFX.
+     * After creating the image in background, its content is copied to {@link #buffer} in
+     * JavaFX thread.
+     */
+    private VolatileImage doubleBuffer;
+
+    /**
+     * The graphic configuration at the time {@link #buffer} has been rendered.
+     * This will be used for creating compatible {@link VolatileImage} for updating.
+     */
+    private GraphicsConfiguration bufferConfiguration;
+
+    /**
      * Wraps {@link #buffer} data array for use by JavaFX images. This is the mechanism used
      * by JavaFX 13+ for allowing {@link #image} to share the same data than {@link #buffer}.
      * The same wrapper can be used for many {@link WritableImage} instances (e.g. thumbnails).
@@ -134,14 +162,30 @@ final class CoverageView extends PlanarCanvas {
     private final AffineTransform dataToImage;
 
     /**
+     * Incremented when {@link #data} changed.
+     *
+     * @see #renderedDataStamp
+     */
+    private int dataChangeCount;
+
+    /**
+     * Value of {@link #dataChangeCount} last time the data have been rendered. This is used
for deciding
+     * if a call to {@link #repaint()} should be done with the next layout operation. We
need this check
+     * for avoiding never-ending repaint events caused by calls to {@link ImageView#setImage(Image)}
+     * causing themselves new layout events.
+     */
+    private int renderedDataStamp;
+
+    /**
      * Creates a new two-dimensional canvas for {@link RenderedImage}.
      */
     public CoverageView() {
         super(Locale.getDefault());
-        coverageProperty    = new SimpleObjectProperty<>(this, "coverage");
-        sliceExtentProperty = new SimpleObjectProperty<>(this, "sliceExtent");
-        dataAlternatives    = new EnumMap<>(RangeType.class);
-        dataToImage = new AffineTransform();
+        coverageProperty       = new SimpleObjectProperty<>(this, "coverage");
+        sliceExtentProperty    = new SimpleObjectProperty<>(this, "sliceExtent");
+        dataAlternatives       = new EnumMap<>(RangeType.class);
+        dataToImage            = new AffineTransform();
+        currentDataAlternative = RangeType.DECLARED;
         view = new Pane() {
             @Override protected void layoutChildren() {
                 super.layoutChildren();
@@ -162,6 +206,14 @@ final class CoverageView extends PlanarCanvas {
     }
 
     /**
+     * Returns the data which are the source of all alternative images that may be stored
in the
+     * {@link #dataAlternatives} map. All alternative images are computed from this source.
+     */
+    private RenderedImage getSourceData() {
+        return dataAlternatives.get(RangeType.DECLARED);
+    }
+
+    /**
      * Returns the region containing the image view.
      * The subclass is implementation dependent and may change in any future version.
      *
@@ -222,6 +274,19 @@ final class CoverageView extends PlanarCanvas {
     }
 
     /**
+     * Starts a background task for loading data, computing slice or rendering the data in
a {@link CoverageView}.
+     *
+     * <p>Tasks need to be careful to not use any {@link CoverageView} field in their
{@link Task#call()} method
+     * (needed fields shall be copied in the JavaFX thread before the background thread is
started).
+     * But {@link Task#succeeded()} and similar methods can read and write those fields.</p>
+     */
+    private void execute(final Task<?> task) {
+        // TODO: set a listener on task.state property.
+        task.setOnFailed((e) -> errorOccurred(e.getSource().getException()));
+        BackgroundThreads.execute(task);
+    }
+
+    /**
      * Invoked when a new coverage has been specified or when the slice extent changed.
      *
      * @param  property  the {@link #coverageProperty} or {@link #sliceExtentProperty} (ignored).
@@ -230,126 +295,287 @@ final class CoverageView extends PlanarCanvas {
      */
     private void onImageSpecified(final ObservableValue<?> property, final Object previous,
final Object value) {
         image.setImage(null);
-        data   = null;
-        buffer = null;
+        data = null;
         dataAlternatives.clear();
         final GridCoverage coverage = getCoverage();
-        if (coverage != null) {
-            data = coverage.render(getSliceExtent());     // TODO: background thread.
-            dataAlternatives.put(RangeType.DECLARED, data);
-            view.requestLayout();
+        if (coverage == null) {
+            buffer        = null;           // Free memory.
+            bufferWrapper = null;
+        } else {
+            final GridExtent sliceExtent = getSliceExtent();
+            execute(new Task<RenderedImage>() {
+                /** Invoked in background thread for fetching the image. */
+                @Override protected RenderedImage call() {
+                    return coverage.render(sliceExtent);
+                }
+
+                /** Invoked in JavaFX thread on success. */
+                @Override protected void succeeded() {
+                    super.succeeded();
+                    if (coverage.equals(getCoverage()) && Objects.equals(sliceExtent,
getSliceExtent())) {
+                        setImage(RangeType.DECLARED, getValue());
+                        setRangeType(currentDataAlternative);
+                    }
+                }
+            });
         }
     }
 
     /**
-     * Sets the background, as a color for now but more patterns my be allowed in a future
version.
+     * Invoked when the user selected a new range of values to scale. Also invoked {@linkplain
#onImageSpecified after
+     * loading a new image or a new slice} for switching the new image to the same type of
range as previously selected.
+     * If the image for the specified type is not already available, then this method computes
the image in a background
+     * thread and refreshes the view after the computation completed.
      */
-    final void setBackground(final Color color) {
-        view.setBackground(new Background(new BackgroundFill(color, null, null)));
+    final void setRangeType(final RangeType rangeType) {
+        currentDataAlternative = rangeType;
+        final RenderedImage alt = dataAlternatives.get(rangeType);
+        if (alt != null) {
+            setImage(rangeType, alt);
+        } else {
+            final RenderedImage source = getSourceData();
+            if (source != null) {
+                execute(new Task<RenderedImage>() {
+                    /** Invoked in background thread for fetching the image. */
+                    @Override protected RenderedImage call() {
+                        switch (rangeType) {
+                            case AUTOMATIC: return ImageRenderings.automaticScale(source);
+                            default:        return source;
+                        }
+                    }
+
+                    /** Invoked in JavaFX thread on success. */
+                    @Override protected void succeeded() {
+                        super.succeeded();
+                        if (source.equals(getSourceData())) {
+                            setImage(rangeType, getValue());
+                        }
+                    }
+                });
+            }
+        }
     }
 
     /**
-     * Invoked when the user selected a new range of values to scale.
+     * Invoked in JavaFX thread for setting the image to show. The given image should be
a slice
+     * produced by current value of {@link #coverageProperty} (should be verified by the
caller).
+     *
+     * @param  type  the type of range used for scaling the color ramp of given image.
+     * @param  alt   the image or alternative image to show (can be {@code null}).
      */
-    final void onRangeTypeChanged(final ObservableValue<? extends RangeType> property,
-                                  final RangeType previous, final RangeType value)
-    {
-        RenderedImage alt = dataAlternatives.get(value);
-        if (alt == null) {
-            alt = dataAlternatives.get(RangeType.DECLARED);     // Source needed by all alternatives.
-            if (value == RangeType.AUTOMATIC) {
-                alt = ImageRenderings.automaticScale(alt);
-            }
-        }
+    private void setImage(final RangeType type, RenderedImage alt) {
+        /*
+         * Store the result but do not necessarily show it because maybe the user changed
the
+         * `RangeType` during the time the background thread was working. If the user did
not
+         * changed the type, then the `alt` variable below will stay unchanged.
+         */
+        dataAlternatives.put(type, alt);
+        alt = dataAlternatives.get(currentDataAlternative);
         if (alt != data) {
             data = alt;
+            dataChangeCount++;
             view.requestLayout();
         }
     }
 
     /**
+     * Sets the background, as a color for now but more patterns my be allowed in a future
version.
+     */
+    final void setBackground(final Color color) {
+        view.setBackground(new Background(new BackgroundFill(color, null, null)));
+    }
+
+    /**
      * Invoked when the {@link #data} content needs to be rendered again into {@link #image}.
      * It may be because a new image has been specified, or because the viewed region moved
      * or have been zoomed.
      */
     private void repaint() {
+        if (dataChangeCount == renderedDataStamp) {
+            return;                                 // Nothing changed since last rendering.
+        }
+        renderedDataStamp = dataChangeCount;
+        final RenderedImage data = this.data;       // Need to copy this reference here before
background tasks.
+        if (data == null) {
+            return;
+        }
         final int width  = Numerics.clamp(Math.round(view.getWidth()));
         final int height = Numerics.clamp(Math.round(view.getHeight()));
         if (width <= 0 || height <= 0) {
             return;
         }
-        PixelBuffer<IntBuffer> wrapper = bufferWrapper;
-        BufferedImage drawTo = buffer;
-        if (drawTo == null || drawTo.getWidth() != width || drawTo.getHeight() != height)
{
-            /*
-             * TODO: run in background.
-             */
-            drawTo = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
-            final Graphics2D gr = drawTo.createGraphics();
-            try {
-                gr.drawRenderedImage(data, dataToImage);
-            } finally {
-                gr.dispose();
-            }
-            /*
-             * The call to `array.getData()` below should be after we finished drawing in
the new
-             * BufferedImage, because this direct access to data array disables GPU accelerations.
-             */
-            final DataBufferInt array = (DataBufferInt) drawTo.getRaster().getDataBuffer();
-            IntBuffer ib = IntBuffer.wrap(array.getData(), array.getOffset(), array.getSize());
-            wrapper = new PixelBuffer<>(width, height, ib, PixelFormat.getIntArgbPreInstance());
-            final WritableImage target = new WritableImage(wrapper);
-            image.setImage(target);
-            bufferWrapper = wrapper;
-            buffer = drawTo;
+        /*
+         * There is two possible situations: if the current buffers are not suitable, we
clear everything related
+         * to Java2D buffered images and will recreate everything from scratch in the background
thread. There is
+         * no need for double-buffering in such case since the new `BufferedImage` will not
be shared with JavaFX
+         * image before the end of this task.
+         *
+         * The second situation is if the buffers are still valid. In such case we should
not update the BufferedImage
+         * in a background thread because the internal array of that image is shared with
JavaFX image, and that image
+         * should be updated only in JavaFX thread through the `PixelBuffer.update(…)`
method. For that second case we
+         * will use a `VolatileImage` as a temporary buffer.
+         *
+         * In both cases we need to be careful to not use directly any `CoverageView` field
from the `call()` method.
+         * Information needed by `call()` must be copied first. This is the case of `dataToImage`
below among others.
+         */
+        final AffineTransform dataToImage = new AffineTransform(this.dataToImage);
+        if (buffer == null || buffer.getWidth() != width || buffer.getHeight() != height)
{
+            buffer              = null;
+            doubleBuffer        = null;
+            bufferWrapper       = null;
+            bufferConfiguration = null;
+            execute(new Task<WritableImage>() {
+                /**
+                 * The Java2D image where to do the rendering. This image will be created
in a background thread
+                 * and assigned to the {@link CoverageView#buffer} field in JavaFX thread
if rendering succeed.
+                 */
+                private BufferedImage drawTo;
+
+                /**
+                 * Wrapper around {@link #buffer} internal array for interoperability between
Java2D and JavaFX.
+                 * Created only if {@link #drawTo} have been successfully painted.
+                 */
+                private PixelBuffer<IntBuffer> wrapper;
+
+                /**
+                 * The graphic configuration at the time {@link #drawTo} has been rendered.
+                 * This will be used for creating {@link VolatileImage} when updating the
image.
+                 */
+                private GraphicsConfiguration configuration;
+
+                /**
+                 * Invoked in background thread for creating and rendering the image (may
be slow).
+                 * Any {@link CoverageView} field needed by this method shall be copied before
the
+                 * background thread is executed; no direct reference to {@link CoverageView}
here.
+                 */
+                @Override
+                protected WritableImage call() {
+                    drawTo = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
+                    final Graphics2D gr = drawTo.createGraphics();
+                    configuration = gr.getDeviceConfiguration();
+                    try {
+                        gr.drawRenderedImage(data, dataToImage);
+                    } finally {
+                        gr.dispose();
+                    }
+                    /*
+                     * The call to `array.getData()` below should be after we finished drawing
in the new
+                     * BufferedImage, because this direct access to data array disables GPU
accelerations.
+                     */
+                    final DataBufferInt array = (DataBufferInt) drawTo.getRaster().getDataBuffer();
+                    IntBuffer ib = IntBuffer.wrap(array.getData(), array.getOffset(), array.getSize());
+                    wrapper = new PixelBuffer<>(width, height, ib, PixelFormat.getIntArgbPreInstance());
+                    return new WritableImage(wrapper);
+                }
+
+                /**
+                 * Invoked in JavaFX thread on success. The JavaFX image is set to the result,
then intermediate
+                 * buffers created by this task are saved in {@link CoverageView} fields
for reuse next time that
+                 * an image of the same size will be rendered again.
+                 */
+                @Override
+                protected void succeeded() {
+                    super.succeeded();
+                    image.setImage(getValue());
+                    buffer              = drawTo;
+                    bufferWrapper       = wrapper;
+                    bufferConfiguration = configuration;
+                }
+            });
         } else {
             /*
-             * Reuse existing resources (JavaFX image and Java2D buffered image).
-             *
-             * TODO: start a background task instead of invoking `updateBuffer` now.
+             * This is the second case described in the block comment at the beginning of
this method:
+             * The existing resources (JavaFX image and Java2D volatile/buffered image) can
be reused.
+             * The Java2D volatile image will be rendered in background thread, then its
content will
+             * be transferred to JavaFX image (through BufferedImage shared array) in JavaFX
thread.
              */
-            wrapper.updateBuffer(new Updater(drawTo));
+            final VolatileImage         previousBuffer = doubleBuffer;
+            final GraphicsConfiguration configuration  = bufferConfiguration;
+            final class Updater extends Task<VolatileImage> implements Callback<PixelBuffer<IntBuffer>,
Rectangle2D> {
+                /**
+                 * Invoked in background thread for rendering the image (may be slow).
+                 * Any {@link CoverageView} field needed by this method shall be copied before
the
+                 * background thread is executed; no direct reference to {@link CoverageView}
here.
+                 */
+                @Override
+                protected VolatileImage call() {
+                    VolatileImage drawTo = previousBuffer;
+                    if (drawTo == null) {
+                        drawTo = configuration.createCompatibleVolatileImage(width, height);
+                    }
+                    boolean invalid = true;
+                    try {
+                        do {
+                            if (drawTo.validate(configuration) == VolatileImage.IMAGE_INCOMPATIBLE)
{
+                                drawTo = configuration.createCompatibleVolatileImage(width,
height);
+                            }
+                            final Graphics2D gr = drawTo.createGraphics();
+                            try {
+                                gr.setBackground(ColorModelFactory.TRANSPARENT);
+                                gr.clearRect(0, 0, drawTo.getWidth(), drawTo.getHeight());
+                                gr.drawRenderedImage(data, dataToImage);
+                            } finally {
+                                gr.dispose();
+                            }
+                            invalid = drawTo.contentsLost();
+                        } while (invalid && !isCancelled());
+                    } finally {
+                        if (invalid) {
+                            drawTo.flush();         // Release native resources.
+                        }
+                    }
+                    return drawTo;
+                }
+
+                /**
+                 * Invoked in JavaFX thread on success. The JavaFX image is set to the result,
then the
+                 * double buffer created by this task is saved in {@link CoverageView} fields
for reuse
+                 * next time that an image of the same size will be rendered again.
+                 */
+                @Override
+                protected void succeeded() {
+                    final VolatileImage drawTo = getValue();
+                    doubleBuffer = drawTo;
+                    try {
+                        bufferWrapper.updateBuffer(this);       // This will invoke the `call(…)`
method below.
+                    } finally {
+                        drawTo.flush();
+                    }
+                    super.succeeded();
+                }
+
+                /**
+                 * Invoked by {@link PixelBuffer#updateBuffer(Callback)} for updating the
{@link #buffer} content.
+                 */
+                @Override
+                public Rectangle2D call(final PixelBuffer<IntBuffer> wrapper) {
+                    final VolatileImage drawTo = doubleBuffer;
+                    final Graphics2D gr = buffer.createGraphics();
+                    final boolean contentsLost;
+                    try {
+                        gr.drawImage(drawTo, 0, 0, null);
+                        contentsLost = drawTo.contentsLost();
+                    } finally {
+                        gr.dispose();
+                    }
+                    if (contentsLost) {
+                        repaint();
+                    }
+                    return null;
+                }
+            }
+            execute(new Updater());
         }
     }
 
     /**
-     * The task for updating an image by reusing existing resources (JavaFX image and Java2D
buffered image).
-     * This task is used when the image size did not changed.
+     * Invoked when an error occurred.
+     *
+     * @param  ex  the exception that occurred.
      *
-     * @todo Extend {@code Task}, write in a background thread in a {@link VolatileImage}
(may be long especially
-     *       if {@link RenderedImage} tiles are computed on-the-fly or if the color model
is not natively supported),
-     *       copy the data to {@link BufferedImage} in the JavaFX thread.
+     * @todo should do something better (e.g. show in status bar).
      */
-    private final class Updater implements Callback<PixelBuffer<IntBuffer>, Rectangle2D>
{
-        /**
-         * The Java2D image which is sharing data with the JavaFX image.
-         * This image needs to be updated in a call to {@link PixelBuffer#updateBuffer(Callback)}.
-         */
-        private final BufferedImage buffer;
-
-        /**
-         * Creates a new updater.
-         */
-        Updater(final BufferedImage buffer) {
-            this.buffer = buffer;
-        }
-
-        /**
-         * Invoked by {@link PixelBuffer#updateBuffer(Callback)} for updating the {@link
#buffer} content.
-         *
-         * @todo We should render {@link #data} in this method since it may be costly.
-         */
-        @Override
-        public Rectangle2D call(final PixelBuffer<IntBuffer> wrapper) {
-            final Graphics2D gr = buffer.createGraphics();
-            try {
-                gr.setBackground(ColorModelFactory.TRANSPARENT);
-                gr.clearRect(0, 0, buffer.getWidth(), buffer.getHeight());
-                gr.drawRenderedImage(data, dataToImage);
-            } finally {
-                gr.dispose();
-            }
-            return null;
-        }
+    private void errorOccurred(final Throwable ex) {
+        ExceptionReporter.show(null, null, ex);
     }
 }


Mime
View raw message