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