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 2020/04/11 21:18:26 UTC

[sis] 03/03: separate the Java2D-specific rendering code in MapCanvasAWT subclass. It allows to use MapCanvas base class with pure JavaFX graphics.

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 f2f577180863a162b48bdc9563e1a80f80ce2d84
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 11 23:15:31 2020 +0200

    separate the Java2D-specific rendering code in MapCanvasAWT subclass.
    It allows to use MapCanvas base class with pure JavaFX graphics.
---
 .../org/apache/sis/gui/coverage/CoverageView.java  |   4 +-
 .../java/org/apache/sis/gui/map/MapCanvas.java     | 522 ++++++---------------
 .../java/org/apache/sis/gui/map/MapCanvasAWT.java  | 460 ++++++++++++++++++
 .../java/org/apache/sis/gui/map/package-info.java  |   4 +-
 4 files changed, 607 insertions(+), 383 deletions(-)

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 e3bbf6d..2d558aa 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
@@ -40,10 +40,10 @@ 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.gui.ImageRenderings;
-import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.gui.map.MapCanvasAWT;
 
 
 /**
@@ -54,7 +54,7 @@ import org.apache.sis.referencing.operation.transform.MathTransforms;
  * @since   1.1
  * @module
  */
-final class CoverageView extends MapCanvas {
+final class CoverageView extends MapCanvasAWT {
     /**
      * The data shown in this view. Note that setting this property to a non-null value may not
      * modify the view content immediately. Instead, a background process will request the tiles.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index ce27237..8bcb84b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -17,22 +17,9 @@
 package org.apache.sis.gui.map;
 
 import java.util.Locale;
-import java.nio.IntBuffer;
-import java.awt.Graphics2D;
-import java.awt.GraphicsEnvironment;
-import java.awt.GraphicsConfiguration;
 import java.awt.geom.AffineTransform;
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.awt.image.RenderedImage;
-import java.awt.image.VolatileImage;
 import javafx.geometry.Bounds;
 import javafx.geometry.Point2D;
-import javafx.geometry.Rectangle2D;
-import javafx.scene.image.ImageView;
-import javafx.scene.image.PixelBuffer;
-import javafx.scene.image.PixelFormat;
-import javafx.scene.image.WritableImage;
 import javafx.scene.layout.Pane;
 import javafx.scene.layout.StackPane;
 import javafx.scene.shape.Rectangle;
@@ -47,7 +34,6 @@ import javafx.scene.Cursor;
 import javafx.event.EventType;
 import javafx.beans.Observable;
 import javafx.concurrent.Task;
-import javafx.util.Callback;
 import org.opengis.geometry.Envelope;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
@@ -57,7 +43,6 @@ import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.internal.util.Numerics;
-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.map.PlanarCanvas;
@@ -66,10 +51,38 @@ import org.apache.sis.internal.map.RenderException;
 
 /**
  * A canvas for maps to be rendered on screen in a JavaFX application.
- * The map is rendered using Java2D in a background thread, then copied in a JavaFX image.
- * Java2D is used for rendering the map because it may contain too many elements for a scene graph.
- * After the map has been rendered, other JavaFX nodes can be put on top of the map, typically for
- * controls interacting with the user.
+ * The map may be an arbitrary JavaFX node, typically an {@link javafx.scene.image.ImageView}
+ * or {@link javafx.scene.canvas.Canvas}, which must be supplied by subclasses.
+ * This base class provides handlers for keyboard, mouse, track pad or touch screen events
+ * such as pans, zooms and rotations. The keyboard actions are:
+ *
+ * <table class="sis">
+ *   <caption>Keyboard actions</caption>
+ *   <tr><th>Key</th>          <th>Action</th></tr>
+ *   <tr><td>⇨</td>            <td>Move view to the right</td></tr>
+ *   <tr><td>⇦</td>            <td>Move view to the left</td></tr>
+ *   <tr><td>⇧</td>            <td>Move view to the top</td></tr>
+ *   <tr><td>⇩</td>            <td>Move view to the bottom</td></tr>
+ *   <tr><td>⎇ + ⇨</td>        <td>Rotate clockwise</td></tr>
+ *   <tr><td>⎇ + ⇦</td>        <td>Rotate anticlockwise</td></tr>
+ *   <tr><td>Page down</td>    <td>Zoom in</td></tr>
+ *   <tr><td>Page up</td>      <td>Zoom out</td></tr>
+ *   <tr><td>Home</td>         <td>{@linkplain #reset() Reset}</td></tr>
+ *   <tr><td>Ctrl + above</td> <td>Above actions as a smaller translation, zoom or rotation</td></tr>
+ * </table>
+ *
+ * <h2>Subclassing</h2>
+ * Implementations need to add at least one JavaFX node in the {@link #floatingPane} list of children.
+ * Map rendering involves the following steps:
+ *
+ * <ol>
+ *   <li>{@link #createRenderer()} is invoked in the JavaFX thread. That method shall take a snapshot
+ *     of every information needed for performing the rendering in background.</li>
+ *   <li>{@link Renderer#render()} is invoked in a background thread. That method creates or updates
+ *     the nodes to show in this {@code MapCanvas} but without interacting with the canvas yet.</li>
+ *   <li>{@link Renderer#commit()} is invoked in the JavaFX thread. The nodes prepared by {@code render()}
+ *     can be transferred to {@link #floatingPane} in that method.</li>
+ * </ol>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -95,85 +108,34 @@ public abstract class MapCanvas extends PlanarCanvas {
     private static final double CONTROL_KEY_FACTOR = 10;
 
     /**
-     * Number of milliseconds to wait before to repaint the {@linkplain #image} during gesture events
-     * (zooms, rotations, pans). This delay allows to collect more events before to run a potentially
-     * costly {@link #repaint()}. It does not apply to the immediate feedback that the user gets from
-     * JavaFX (an image with lower quality used until the higher quality image become ready).
+     * Number of milliseconds to wait before to repaint after gesture events (zooms, rotations, pans).
+     * This delay allows to collect more events before to run a potentially costly {@link #repaint()}.
+     * It does not apply to the immediate feedback that the user gets from JavaFX affine transforms
+     * (an image with lower quality used until the higher quality image become ready).
      */
     private static final long REPAINT_DELAY = 500;
 
     /**
-     * Whether to try to get native acceleration in the {@link VolatileImage} used for painting the map.
-     * Native acceleration is of limited interested here because even if painting occurs in video card
-     * memory, it is copied to Java heap before to be transferred to JavaFX image, which may itself copy
-     * back to video card memory. I'm not aware of a way to perform direct transfer from AWT to JavaFX.
-     * Consequently before to enable this acceleration, we should benchmark to see if it is worth.
-     */
-    private static final boolean NATIVE_ACCELERATION = false;
-
-    /**
-     * A buffer where to draw the content of the map for the region to be displayed.
-     * This buffer uses ARGB color model, contrarily to the {@link RenderedImage} of
-     * {@link org.apache.sis.coverage.grid.GridCoverage} which may have any color model.
-     * This buffered image will contain only the visible region of the map;
-     * it may be a zoom over a small region.
-     *
-     * <p>This buffered image contains the same data than the {@linkplain #image} of this canvas.
-     * Those two images will share the same data array (no copy) and the same coordinate system.</p>
-     */
-    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.
-     * This configuration determines whether native acceleration will be enabled or not.
-     *
-     * @see #NATIVE_ACCELERATION
-     */
-    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).
-     */
-    private PixelBuffer<IntBuffer> bufferWrapper;
-
-    /**
-     * The node where the rendered map will be shown. Its content is prepared in a background thread
-     * by {@link Renderer#paint(Graphics2D)}. Subclasses should not set the image content directly.
-     */
-    protected final ImageView image;
-
-    /**
      * The pane showing the map and any other JavaFX nodes to scale and translate together with the map.
-     * This pane contains at least the JavaFX {@linkplain #image} of the map, but more children (shapes,
-     * texts, controls, <i>etc.</i>) can be added by subclasses into the {@link Pane#getChildren()} list.
+     * This pane is initially empty; subclasses should add nodes (canvas, images, shapes, texts, <i>etc.</i>)
+     * into the {@link Pane#getChildren()} list.
      * All children must specify their coordinates in units relative to the pane (absolute layout).
      * Those coordinates can be computed from real world coordinates by {@link #objectiveToDisplay}.
      *
      * <p>This pane contains an {@link Affine} transform which is updated by user gestures such as pans,
-     * zooms or rotations. Visual positions of all children move together is response to user's gesture,
+     * zooms or rotations. Visual positions of all children move together in response to user's gesture,
      * thus giving an appearance of pane floating around. Changes in {@code floatingPane} affine transform
-     * are temporary; they are applied for producing immediate visual feedback while the map {@linkplain #image}
-     * is recomputed in a background thread. Once calculation is completed and {@linkplain #image} content replaced,
+     * are temporary; they are applied for producing immediate visual feedback while the map is recomputed
+     * in a background thread. Once calculation is completed and the content of this pane has been updated,
      * the {@code floatingPane} {@link Affine} transform is reset to identity.</p>
      */
     protected final Pane floatingPane;
 
     /**
      * The pane showing the map and other JavaFX nodes to keep at fixed position regardless pans, zooms or rotations
-     * applied on the map. This pane contains at least the {@linkplain #floatingPane} (which itself contains the map
-     * {@linkplain #image}), but more children (shapes, texts, controls, <i>etc.</i>) can be added by subclasses into
-     * the {@link StackPane#getChildren()} list.
+     * applied on the map. This pane contains at least the {@linkplain #floatingPane} (which itself contains the map),
+     * but more children (shapes, texts, controls, <i>etc.</i>) can be added by subclasses into the
+     * {@link StackPane#getChildren()} list.
      */
     protected final StackPane fixedPane;
 
@@ -197,7 +159,7 @@ public abstract class MapCanvas extends PlanarCanvas {
     /**
      * Value of {@link #contentChangeCount} 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
+     * avoiding never-ending repaint events caused by calls to {@code ImageView.setImage(Image)} causing
      * themselves new layout events. It is okay if this value overflows.
      */
     private int renderedContentStamp;
@@ -222,25 +184,24 @@ public abstract class MapCanvas extends PlanarCanvas {
     private boolean invalidObjectiveToDisplay;
 
     /**
-     * The zooms, pans and rotations applied on {@link #floatingPane} since last time the {@linkplain #image}
-     * has been painted. This is the identity transform except during the short time between a gesture (zoom,
-     * pan, <i>etc.</i>) and the completion of latest {@link #repaint()} event.
-     * This is used for giving immediate feedback to the user while waiting for the new image to be ready.
-     * Since this transform is a member of the floating pane {@linkplain Pane#getTransforms() transform list},
-     * changes in this transform are immediately visible to the user.
+     * The zooms, pans and rotations applied on {@link #floatingPane} since last time the map has been painted.
+     * This is the identity transform except during the short time between a gesture (zoom, pan, <i>etc.</i>)
+     * and the completion of latest {@link #repaint()} event. This is used for giving immediate feedback to user
+     * while waiting for the new rendering to be ready. Since this transform is a member of {@link #floatingPane}
+     * {@linkplain Pane#getTransforms() transform list}, changes in this transform are immediately visible to user.
      */
     private final Affine transform;
 
     /**
      * The {@link #transform} values at the time the {@link #repaint()} method has been invoked.
-     * This is a change applied on {@link #objectiveToDisplay} but not yet visible in the image.
-     * After the image has been updated, this transform is reset to identity.
+     * This is a change applied on {@link #objectiveToDisplay} but not yet visible in the map.
+     * After the map has been updated, this transform is reset to identity.
      */
     private final Affine changeInProgress;
 
     /**
-     * The value to assign to {@link #transform} after the {@linkplain #image} has been replaced
-     * or updated with a new content.
+     * The value to assign to {@link #transform} after the {@link #floatingPane} has been updated
+     * with transformed content.
      */
     private final Affine transformOnNewImage;
 
@@ -259,12 +220,10 @@ public abstract class MapCanvas extends PlanarCanvas {
      */
     public MapCanvas(final Locale locale) {
         super(locale);
-        image = new ImageView();
-        image.setPreserveRatio(true);
         transform           = new Affine();
         changeInProgress    = new Affine();
         transformOnNewImage = new Affine();
-        final Pane view = new Pane(image) {
+        final Pane view = new Pane() {
             @Override protected void layoutChildren() {
                 super.layoutChildren();
                 if (contentsChanged()) {
@@ -470,7 +429,7 @@ public abstract class MapCanvas extends PlanarCanvas {
     /**
      * Returns {@code true} if content changed since the last {@link #repaint()} execution.
      */
-    private boolean contentsChanged() {
+    final boolean contentsChanged() {
         return contentChangeCount != renderedContentStamp;
     }
 
@@ -491,30 +450,8 @@ public abstract class MapCanvas extends PlanarCanvas {
     }
 
     /**
-     * Starts a background task for any process for loading or rendering the map.
-     * This {@code MapCanvas} class invokes this method for rendering the map,
-     * but subclasses can also invoke this method for other purposes.
-     *
-     * <p>Tasks need to be careful to not access any {@code MapCanvas} property in their {@link Task#call()} method.
-     * If a canvas property is needed by the task, its value should be copied before the background thread is started.
-     * However {@link Task#succeeded()} and similar methods can safety read and write those properties.</p>
-     *
-     * <p>Subclasses are encouraged to override this method and configure the following properties
-     * before to invoke {@code super.execute(task)}:</p>
-     * <ul>
-     *   <li><code>{@linkplain Task#runningProperty()}.addListener(…)</code></li>
-     *   <li><code>{@linkplain Task#setOnFailed Task.setOnFailed}(…)</code></li>
-     * </ul>
-     *
-     * @param  task  the task to execute in a background thread for loading or rendering the map.
-     */
-    protected void execute(final Task<?> task) {
-        BackgroundThreads.execute(task);
-    }
-
-    /**
      * Invoked in JavaFX thread for creating a renderer to be executed in a background thread.
-     * Subclasses should copy in this method all {@code MapCanvas} properties that the background thread
+     * Subclasses shall copy in this method all {@code MapCanvas} properties that the background thread
      * will need for performing the rendering process.
      *
      * @return rendering process to be executed in background thread,
@@ -523,9 +460,19 @@ public abstract class MapCanvas extends PlanarCanvas {
     protected abstract Renderer createRenderer();
 
     /**
-     * A snapshot of {@link MapCanvas} state to paint as an image.
-     * The snapshot is created in JavaFX thread by the {@link MapCanvas#createRenderer()} method,
-     * then the rendering process is executed in a background thread by {@link #paint(Graphics2D)}.
+     * A snapshot of {@link MapCanvas} state to render as a map, together with rendering code.
+     * This class is instantiated and used as below:
+     *
+     * <ol>
+     *   <li>{@link MapCanvas} invokes {@link MapCanvas#createRenderer()} in the JavaFX thread.
+     *     That method shall take a snapshot of every information needed for performing the rendering
+     *     in a background thread.</li>
+     *   <li>{@link MapCanvas} invokes {@link #render()} in a background thread. That method creates or
+     *     updates the nodes to show in the canvas but without reading or writing any canvas property;
+     *     that method should use only the snapshot taken in step 1.</li>
+     *   <li>{@link MapCanvas} invokes {@link #commit()} in the JavaFX thread. The nodes prepared at
+     *     step 2 can be transferred to {@link MapCanvas#floatingPane} in that method.</li>
+     * </ol>
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @version 1.1
@@ -540,7 +487,7 @@ public abstract class MapCanvas extends PlanarCanvas {
 
         /**
          * Creates a new renderer. The {@linkplain #getWidth() width} and {@linkplain #getHeight() height}
-         * are initially zero; they will get a non-zero values before {@link #paint(Graphics2D)} is invoked.
+         * are initially zero; they will get a non-zero values before {@link #render()} is invoked.
          */
         protected Renderer() {
         }
@@ -556,16 +503,9 @@ public abstract class MapCanvas extends PlanarCanvas {
         }
 
         /**
-         * Returns whether the given buffer is non-null and have the expected size.
-         */
-        final boolean isValid(final BufferedImage buffer) {
-            return buffer != null && buffer.getWidth() == width && buffer.getHeight() == height;
-        }
-
-        /**
          * Returns the width (number of columns) of the view, in pixels.
          *
-         * @return number of columns in the image to paint.
+         * @return number of pixels to render horizontally.
          */
         public int getWidth() {
             return width;
@@ -574,7 +514,7 @@ public abstract class MapCanvas extends PlanarCanvas {
         /**
          * Returns the height (number of rows) of the view, in pixels.
          *
-         * @return number of rows in the image to paint.
+         * @return number of pixels to render vertically.
          */
         public int getHeight() {
             return height;
@@ -583,12 +523,19 @@ public abstract class MapCanvas extends PlanarCanvas {
         /**
          * Invoked in a background thread for rendering the map. This method should not access any
          * {@link MapCanvas} property; if some canvas properties are needed, they should have been
-         * copied at construction time. This method may be invoked many times if the rendering is
-         * done in a {@link VolatileImage}.
+         * copied at construction time.
+         */
+        protected abstract void render();
+
+        /**
+         * Invoked in JavaFX thread after {@link #render()} completion. This method can update the
+         * {@link #floatingPane} children with the nodes (images, shaped, <i>etc.</i>) created by
+         * {@link #render()}.
          *
-         * @param  gr  the Java2D handler to use for rendering the map.
+         * @return {@code true} on success, or {@code false} if the rendering should be redone
+         *         (for example because a change has been detected in the data).
          */
-        protected abstract void paint(Graphics2D gr);
+        protected abstract boolean commit();
     }
 
     /**
@@ -624,13 +571,12 @@ public abstract class MapCanvas extends PlanarCanvas {
     }
 
     /**
-     * Invoked when the map content needs to be rendered again into the {@link #image}.
-     * It may be because the map has new content, or because the viewed region moved or
-     * has been zoomed.
+     * Invoked when the map content needs to be rendered again.
+     * It may be because the map has new content, or because the viewed region moved or has been zoomed.
      *
      * @see #requestRepaint()
      */
-    private void repaint() {
+    final void repaint() {
         /*
          * If a rendering is already in progress, do not send a new request now.
          * Wait for current rendering to finish; a new one will be automatically
@@ -700,251 +646,46 @@ public abstract class MapCanvas extends PlanarCanvas {
          * may take a snapshot of current canvas state in preparation for use in background threads.
          */
         final Renderer context = createRenderer();
-        if (context == null || !context.initialize(floatingPane)) {
-            return;
-        }
-        /*
-         * 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 when the buffer is 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 `MapCanvas` field from the `call()` method.
-         * Information needed by `call()` must be copied first.
-         */
-        final Task<?> worker;
-        if (!context.isValid(buffer)) {
-            buffer              = null;
-            doubleBuffer        = null;
-            bufferWrapper       = null;
-            bufferConfiguration = null;
-            worker = new Creator(context);
-        } else {
-            worker = new Updater(context);
+        if (context != null && context.initialize(floatingPane)) {
+            executeRendering(createWorker(context));
         }
-        executeRendering(worker);
     }
 
     /**
-     * Background tasks for creating a new {@link BufferedImage}. This task is invoked when there is no
-     * previous resources that we can recycle, either because they have never been created yet or because
-     * they are not suitable anymore (for example because the image size changed).
+     * Creates the background task which will invoke {@link Renderer#render()} in a background thread.
+     * The tasks must invoke {@link #renderingCompleted()} in JavaFX thread after completion, either
+     * successful or not.
      */
-    private final class Creator extends Task<WritableImage> {
-        /**
-         * The user-provided object which will perform the actual rendering.
-         * Its {@link Renderer#paint(Graphics2D)} method will be invoked.
-         */
-        private final Renderer renderer;
-
-        /**
-         * The Java2D image where to do the rendering. This image will be created in a background thread
-         * and assigned to the {@link MapCanvas#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;
-
-        /**
-         * Creates a new task for painting without resource recycling.
-         */
-        Creator(final Renderer context) {
-            renderer = context;
-        }
-
-        /**
-         * Invoked in background thread for creating and rendering the image (may be slow).
-         * Any {@link MapCanvas} property needed by this method shall be copied before the
-         * background thread is executed; no direct reference to {@link MapCanvas} here.
-         */
-        @Override
-        protected WritableImage call() {
-            final int width  = renderer.getWidth();
-            final int height = renderer.getHeight();
-            drawTo = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
-            final Graphics2D gr = drawTo.createGraphics();
-            try {
-                configuration = gr.getDeviceConfiguration();
-                renderer.paint(gr);
-            } finally {
-                gr.dispose();
-            }
-            if (NATIVE_ACCELERATION) {
-                if (!configuration.getImageCapabilities().isAccelerated()) {
-                    configuration = GraphicsEnvironment.getLocalGraphicsEnvironment()
-                                    .getDefaultScreenDevice().getDefaultConfiguration();
-                }
+    Task<?> createWorker(final Renderer renderer) {
+        return new Task<Void>() {
+            /** Invoked in background thread. */
+            @Override protected Void call() {
+                renderer.render();
+                return null;
             }
-            /*
-             * 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 MapCanvas} fields for reuse next time that
-         * an image of the same size will be rendered again.
-         */
-        @Override
-        protected void succeeded() {
-            image.setImage(getValue());
-            buffer              = drawTo;
-            bufferWrapper       = wrapper;
-            bufferConfiguration = configuration;
-            imageUpdated();
-            if (contentsChanged()) {
-                repaint();
-            }
-        }
-
-        @Override protected void failed()    {imageUpdated();}
-        @Override protected void cancelled() {imageUpdated();}
-    }
 
-    /**
-     * Background tasks for painting in an existing {@link BufferedImage}. This task is invoked
-     * when previous 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 {@link BufferedImage} shared array) in JavaFX thread.
-     */
-    private final class Updater extends Task<VolatileImage> implements Callback<PixelBuffer<IntBuffer>, Rectangle2D> {
-        /**
-         * The user-provided object which will perform the actual rendering.
-         * Its {@link Renderer#paint(Graphics2D)} method will be invoked.
-         */
-        private final Renderer renderer;
-
-        /**
-         * The buffer during last paint operation. This buffer will be reused if possible,
-         * but may become invalid and in need to be recreated. May be {@code null}.
-         */
-        private VolatileImage previousBuffer;
-
-        /**
-         * The configuration to use for creating a new {@link VolatileImage}
-         * if {@link #previousBuffer} is invalid.
-         */
-        private final GraphicsConfiguration configuration;
-
-        /**
-         * Whether {@link VolatileImage} content became invalid and needs to be recreated.
-         */
-        private boolean contentsLost;
-
-        /**
-         * Creates a new task for painting with resource recycling.
-         */
-        Updater(final Renderer context) {
-            renderer       = context;
-            previousBuffer = doubleBuffer;
-            configuration  = bufferConfiguration;
-        }
-
-        /**
-         * Invoked in background thread for rendering the image (may be slow).
-         * Any {@link MapCanvas} field needed by this method shall be copied before the
-         * background thread is executed; no direct reference to {@link MapCanvas} here.
-         */
-        @Override
-        protected VolatileImage call() {
-            final int width  = renderer.getWidth();
-            final int height = renderer.getHeight();
-            VolatileImage drawTo = previousBuffer;
-            previousBuffer = null;                      // For letting GC do its work.
-            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());
-                        renderer.paint(gr);
-                    } finally {
-                        gr.dispose();
-                    }
-                    invalid = drawTo.contentsLost();
-                } while (invalid && !isCancelled());
-            } finally {
-                if (invalid) {
-                    drawTo.flush();         // Release native resources.
+            /** Invoked in JavaFX thread on success. */
+            @Override protected void succeeded() {
+                final boolean done = renderer.commit();
+                renderingCompleted();
+                if (!done || contentsChanged()) {
+                    repaint();
                 }
             }
-            return drawTo;
-        }
-
-        /**
-         * 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();
-            try {
-                gr.drawImage(drawTo, 0, 0, null);
-                contentsLost = drawTo.contentsLost();
-            } finally {
-                gr.dispose();
-            }
-            return null;
-        }
-
-        /**
-         * 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 MapCanvas} 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(PixelBuffer)` method above.
-            } finally {
-                drawTo.flush();
-            }
-            imageUpdated();
-            if (contentsLost || contentsChanged()) {
-                repaint();
-            }
-        }
 
-        @Override protected void failed()    {imageUpdated();}
-        @Override protected void cancelled() {imageUpdated();}
+            /** Invoked in JavaFX thread on failure. */
+            @Override protected void failed()    {renderingCompleted();}
+            @Override protected void cancelled() {renderingCompleted();}
+        };
     }
 
     /**
-     * Invoked after the background thread created by {@link #repaint()} finished to update image content.
+     * Invoked after the background thread created by {@link #repaint()} finished to update map content.
      * The {@link #changeInProgress} is the JavaFX transform at the time the repaint event was trigged and
-     * which is now integrated in the image. That transform will be removed from {@link #floatingPane} transforms.
+     * which is now integrated in the map. That transform will be removed from {@link #floatingPane} transforms.
      * It may be identity if no zoom, rotation or pan gesture has been applied since last rendering.
      */
-    private void imageUpdated() {
+    final void renderingCompleted() {
         renderingInProgress = null;
         floatingPane.setCursor(Cursor.CROSSHAIR);
         final Point2D p = changeInProgress.transform(xPanStart, yPanStart);
@@ -992,17 +733,26 @@ public abstract class MapCanvas extends PlanarCanvas {
     }
 
     /**
-     * Clears the image and all intermediate buffer.
-     * Invoking this method may help to release memory when the map is no longer shown.
+     * Starts a background task for any process loading or rendering the map.
+     * This {@code MapCanvas} class invokes this method for rendering the map,
+     * but subclasses can also invoke this method for other purposes.
+     *
+     * <p>Tasks need to be careful to not access any {@code MapCanvas} property in their {@link Task#call()} method.
+     * If a canvas property is needed by the task, its value should be copied before the background thread is started.
+     * However {@link Task#succeeded()} and similar methods can safety read and write those properties.</p>
+     *
+     * <h4>Overriding</h4>
+     * Subclasses can override this method for configuring the task before execution.
+     * For example the following methods may be invoked before to call {@code super.execute(task)}:
+     * <ul>
+     *   <li><code>{@linkplain Task#runningProperty()}.addListener(…)</code></li>
+     *   <li><code>{@linkplain Task#setOnFailed Task.setOnFailed}(…)</code></li>
+     * </ul>
+     *
+     * @param  task  the task to execute in a background thread for loading or rendering the map.
      */
-    protected void clear() {
-        image.setImage(null);
-        buffer              = null;
-        bufferWrapper       = null;
-        doubleBuffer        = null;
-        bufferConfiguration = null;
-        transform.setToIdentity();
-        changeInProgress.setToIdentity();
+    protected void execute(final Task<?> task) {
+        BackgroundThreads.execute(task);
     }
 
     /**
@@ -1014,4 +764,16 @@ public abstract class MapCanvas extends PlanarCanvas {
     protected void errorOccurred(final Throwable ex) {
         ExceptionReporter.show(null, null, ex);
     }
+
+    /**
+     * Clears the map.
+     * Invoking this method may help to release memory when the map is no longer shown.
+     *
+     * @see #reset()
+     */
+    protected void clear() {
+        transform.setToIdentity();
+        changeInProgress.setToIdentity();
+        invalidObjectiveToDisplay = true;
+    }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
new file mode 100644
index 0000000..6ee621a
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
@@ -0,0 +1,460 @@
+/*
+ * 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.gui.map;
+
+import java.util.Locale;
+import java.nio.IntBuffer;
+import java.awt.Graphics2D;
+import java.awt.GraphicsEnvironment;
+import java.awt.GraphicsConfiguration;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.RenderedImage;
+import java.awt.image.VolatileImage;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.PixelBuffer;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.WritableImage;
+import javafx.concurrent.Task;
+import javafx.util.Callback;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+
+
+/**
+ * A canvas for maps to be rendered using Java2D from Abstract Window Toolkit.
+ * The map is rendered using Java2D in a background thread, then copied in a JavaFX image.
+ * Java2D is used for rendering the map because it may contain too many elements for a scene graph.
+ * After the map has been rendered, other JavaFX nodes can be put on top of the map, typically for
+ * controls by the user.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class MapCanvasAWT extends MapCanvas {
+    /**
+     * Whether to try to get native acceleration in the {@link VolatileImage} used for painting the map.
+     * Native acceleration is of limited interested here because even if painting occurs in video card
+     * memory, it is copied to Java heap before to be transferred to JavaFX image, which may itself copy
+     * back to video card memory. I'm not aware of a way to perform direct transfer from AWT to JavaFX.
+     * Consequently before to enable this acceleration, we should benchmark to see if it is worth.
+     */
+    private static final boolean NATIVE_ACCELERATION = false;
+
+    /**
+     * A buffer where to draw the content of the map for the region to be displayed.
+     * This buffer uses ARGB color model, contrarily to the {@link RenderedImage} of
+     * {@link org.apache.sis.coverage.grid.GridCoverage} which may have any color model.
+     * This buffered image will contain only the visible region of the map;
+     * it may be a zoom over a small region.
+     *
+     * <p>This buffered image contains the same data than the {@linkplain #image} of this canvas.
+     * Those two images will share the same data array (no copy) and the same coordinate system.</p>
+     */
+    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.
+     * This configuration determines whether native acceleration will be enabled or not.
+     *
+     * @see #NATIVE_ACCELERATION
+     */
+    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).
+     */
+    private PixelBuffer<IntBuffer> bufferWrapper;
+
+    /**
+     * The node where the rendered map will be shown. Its content is prepared in a background thread
+     * by {@link Renderer#paint(Graphics2D)}. Subclasses should not set the image content directly.
+     */
+    protected final ImageView image;
+
+    /**
+     * Creates a new canvas for JavaFX application.
+     *
+     * @param  locale  the locale to use for labels and some messages, or {@code null} for default.
+     */
+    public MapCanvasAWT(final Locale locale) {
+        super(locale);
+        image = new ImageView();
+        image.setPreserveRatio(true);
+        floatingPane.getChildren().add(image);
+    }
+
+    /**
+     * Invoked in JavaFX thread for creating a renderer to be executed in a background thread.
+     * Subclasses should copy in this method all {@code MapCanvas} properties that the background thread
+     * will need for performing the rendering process.
+     *
+     * @return rendering process to be executed in background thread,
+     *         or {@code null} if there is nothing to paint.
+     */
+    @Override
+    protected abstract Renderer createRenderer();
+
+    /**
+     * A snapshot of {@link MapCanvasAWT} state to paint as an image.
+     * The snapshot is created in JavaFX thread by the {@link MapCanvasAWT#createRenderer()} method,
+     * then the rendering process is executed in a background thread by {@link #paint(Graphics2D)}.
+     * Methods are invoked in the following order:
+     *
+     * <table class="sis">
+     *   <caption>Methods invoked during a map rendering process</caption>
+     *   <tr><th>Method</th>                     <th>Thread</th>            <th>Remarks</th></tr>
+     *   <tr><td>{@link #createRenderer()}</td>  <td>JavaFX thread</td>     <td></td></tr>
+     *   <tr><td>{@link #render()}</td>          <td>Background thread</td> <td></td></tr>
+     *   <tr><td>{@link #paint(Graphics2D)}</td> <td>Background thread</td> <td>May be invoked many times.</td></tr>
+     *   <tr><td>{@link #commit()}</td>          <td>JavaFX thread</td>     <td></td></tr>
+     * </table>
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.1
+     * @since   1.1
+     * @module
+     */
+    protected abstract static class Renderer extends MapCanvas.Renderer {
+        /**
+         * Creates a new renderer. The {@linkplain #getWidth() width} and {@linkplain #getHeight() height}
+         * are initially zero; they will get a non-zero values before {@link #paint(Graphics2D)} is invoked.
+         */
+        protected Renderer() {
+        }
+
+        /**
+         * Returns whether the given buffer is non-null and have the expected size.
+         */
+        final boolean isValid(final BufferedImage buffer) {
+            return (buffer != null)
+                    && buffer.getWidth()  == super.getWidth()
+                    && buffer.getHeight() == super.getHeight();
+        }
+
+        /**
+         * Invoked in a background thread before {@link #paint(Graphics2D)}. Subclasses can override
+         * this method if some rendering steps do not need {@link Graphics2D} handler. Doing work in
+         * advance allow to hold the {@link Graphics2D} handler for a shorter time.
+         *
+         * <p>The default implementation does nothing.</p>
+         */
+        @Override
+        protected void render() {
+        }
+
+        /**
+         * Invoked in a background thread for rendering the map. This method should not access any
+         * {@link MapCanvas} property; if some canvas properties are needed, they should have been
+         * copied at construction time. This method may be invoked many times if the rendering is
+         * done in a {@link VolatileImage}.
+         *
+         * @param  gr  the Java2D handler to use for rendering the map.
+         */
+        protected abstract void paint(Graphics2D gr);
+
+        /**
+         * Invoked in JavaFX thread after {@link #render()} completion. This method can update the
+         * {@link #floatingPane} children with the nodes (images, shaped, <i>etc.</i>) created by
+         * {@link #render()}.
+         *
+         * <p>The default implementation does nothing.</p>
+         *
+         * @return {@code true} on success, or {@code false} if the rendering should be redone
+         *         (for example because a change has been detected in the data).
+         */
+        @Override
+        protected boolean commit() {
+            return true;
+        }
+    }
+
+    /**
+     * Invoked when the map content needs to be rendered again into the {@link #image}.
+     * It may be because the map has new content, or because the viewed region moved or
+     * has been zoomed.
+     *
+     * <p>There is two possible situations:</p>
+     * <ul class="verbose">
+     *   <li>If the current buffers are not suitable, then we clear everything related to Java2D buffered images.
+     *     Those resources will recreated from scratch in background thread. There is no need for double-buffering
+     *     in such case because the new {@link BufferedImage} will not be shared with JavaFX image before the end
+     *     of this task.</li>
+     *   <li>If the current buffer are still valid, then we should not update {@link BufferedImage} in background
+     *     thread because the internal array of that image is shared with JavaFX image. That image can be updated
+     *     only in JavaFX thread through the {@code PixelBuffer.update(…)} method. In this case we will use a
+     *     {@link VolatileImage} as a temporary buffer.</li>
+     * </ul>
+     *
+     * In both cases we need to be careful to not use directly any {@link MapCanvas} field from the {@code call()}
+     * methods. Information needed by {@code call()} must be copied first.
+     *
+     * @see #requestRepaint()
+     */
+    @Override
+    final Task<?> createWorker(final MapCanvas.Renderer mc) {
+        final Renderer context = (Renderer) mc;
+        if (!context.isValid(buffer)) {
+            buffer              = null;
+            doubleBuffer        = null;
+            bufferWrapper       = null;
+            bufferConfiguration = null;
+            return new Creator(context);
+        } else {
+            return new Updater(context);
+        }
+    }
+
+    /**
+     * Background tasks for creating a new {@link BufferedImage}. This task is invoked when there is no
+     * previous resources that we can recycle, either because they have never been created yet or because
+     * they are not suitable anymore (for example because the image size changed).
+     */
+    private final class Creator extends Task<WritableImage> {
+        /**
+         * The user-provided object which will perform the actual rendering.
+         * Its {@link Renderer#paint(Graphics2D)} method will be invoked.
+         */
+        private final Renderer renderer;
+
+        /**
+         * The Java2D image where to do the rendering. This image will be created in a background thread
+         * and assigned to the {@link MapCanvasAWT#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;
+
+        /**
+         * Creates a new task for painting without resource recycling.
+         */
+        Creator(final Renderer context) {
+            renderer = context;
+        }
+
+        /**
+         * Invoked in background thread for creating and rendering the image (may be slow).
+         * Any {@link MapCanvas} property needed by this method shall be copied before the
+         * background thread is executed; no direct reference to {@link MapCanvas} here.
+         */
+        @Override
+        protected WritableImage call() {
+            renderer.render();
+            final int width  = renderer.getWidth();
+            final int height = renderer.getHeight();
+            drawTo = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
+            final Graphics2D gr = drawTo.createGraphics();
+            try {
+                configuration = gr.getDeviceConfiguration();
+                renderer.paint(gr);
+            } finally {
+                gr.dispose();
+            }
+            if (NATIVE_ACCELERATION) {
+                if (!configuration.getImageCapabilities().isAccelerated()) {
+                    configuration = GraphicsEnvironment.getLocalGraphicsEnvironment()
+                                    .getDefaultScreenDevice().getDefaultConfiguration();
+                }
+            }
+            /*
+             * 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 MapCanvas} fields for reuse next time that
+         * an image of the same size will be rendered again.
+         */
+        @Override
+        protected void succeeded() {
+            image.setImage(getValue());
+            buffer              = drawTo;
+            bufferWrapper       = wrapper;
+            bufferConfiguration = configuration;
+            final boolean done  = renderer.commit();
+            renderingCompleted();
+            if (!done || contentsChanged()) {
+                repaint();
+            }
+        }
+
+        @Override protected void failed()    {renderingCompleted();}
+        @Override protected void cancelled() {renderingCompleted();}
+    }
+
+    /**
+     * Background tasks for painting in an existing {@link BufferedImage}. This task is invoked
+     * when previous 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 {@link BufferedImage} shared array) in JavaFX thread.
+     */
+    private final class Updater extends Task<VolatileImage> implements Callback<PixelBuffer<IntBuffer>, Rectangle2D> {
+        /**
+         * The user-provided object which will perform the actual rendering.
+         * Its {@link Renderer#paint(Graphics2D)} method will be invoked.
+         */
+        private final Renderer renderer;
+
+        /**
+         * The buffer during last paint operation. This buffer will be reused if possible,
+         * but may become invalid and in need to be recreated. May be {@code null}.
+         */
+        private VolatileImage previousBuffer;
+
+        /**
+         * The configuration to use for creating a new {@link VolatileImage}
+         * if {@link #previousBuffer} is invalid.
+         */
+        private final GraphicsConfiguration configuration;
+
+        /**
+         * Whether {@link VolatileImage} content became invalid and needs to be recreated.
+         */
+        private boolean contentsLost;
+
+        /**
+         * Creates a new task for painting with resource recycling.
+         */
+        Updater(final Renderer context) {
+            renderer       = context;
+            previousBuffer = doubleBuffer;
+            configuration  = bufferConfiguration;
+        }
+
+        /**
+         * Invoked in background thread for rendering the image (may be slow).
+         * Any {@link MapCanvas} field needed by this method shall be copied before the
+         * background thread is executed; no direct reference to {@link MapCanvas} here.
+         */
+        @Override
+        protected VolatileImage call() {
+            renderer.render();
+            final int width  = renderer.getWidth();
+            final int height = renderer.getHeight();
+            VolatileImage drawTo = previousBuffer;
+            previousBuffer = null;                      // For letting GC do its work.
+            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());
+                        renderer.paint(gr);
+                    } finally {
+                        gr.dispose();
+                    }
+                    invalid = drawTo.contentsLost();
+                } while (invalid && !isCancelled());
+            } finally {
+                if (invalid) {
+                    drawTo.flush();         // Release native resources.
+                }
+            }
+            return drawTo;
+        }
+
+        /**
+         * 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();
+            try {
+                gr.drawImage(drawTo, 0, 0, null);
+                contentsLost = drawTo.contentsLost();
+            } finally {
+                gr.dispose();
+            }
+            return null;
+        }
+
+        /**
+         * 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 MapCanvas} 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(PixelBuffer)` method above.
+            } finally {
+                drawTo.flush();                     // Release native resources.
+            }
+            final boolean done = renderer.commit();
+            renderingCompleted();
+            if (!done || contentsLost || contentsChanged()) {
+                repaint();
+            }
+        }
+
+        @Override protected void failed()    {renderingCompleted();}
+        @Override protected void cancelled() {renderingCompleted();}
+    }
+
+    /**
+     * Clears the image and all intermediate buffer.
+     * Invoking this method may help to release memory when the map is no longer shown.
+     */
+    @Override
+    protected void clear() {
+        image.setImage(null);
+        buffer              = null;
+        bufferWrapper       = null;
+        doubleBuffer        = null;
+        bufferConfiguration = null;
+        super.clear();
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java
index c9596d8..19e49b1 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java
@@ -17,7 +17,9 @@
 
 /**
  * Widgets for showing a map in a JavaFX application.
- * This package is a first draft to be completed in future Apache SIS versions.
+ * {@link org.apache.sis.gui.map.MapCanvas} is the base class for painting a map using arbitrary
+ * JavaFX nodes such as {@link javafx.scene.image.ImageView} or {@link javafx.scene.canvas.Canvas}.
+ * {@link org.apache.sis.gui.map.MapCanvasAWT} is a specialization for painting the map using Java2D.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1