From commits-return-13462-apmail-sis-commits-archive=sis.apache.org@sis.apache.org Sat Apr 11 21:18:26 2020 Return-Path: X-Original-To: apmail-sis-commits-archive@www.apache.org Delivered-To: apmail-sis-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [207.244.88.153]) by minotaur.apache.org (Postfix) with SMTP id 1102219572 for ; Sat, 11 Apr 2020 21:18:25 +0000 (UTC) Received: (qmail 77783 invoked by uid 500); 11 Apr 2020 21:18:25 -0000 Delivered-To: apmail-sis-commits-archive@sis.apache.org Received: (qmail 77749 invoked by uid 500); 11 Apr 2020 21:18:24 -0000 Mailing-List: contact commits-help@sis.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: sis-dev@sis.apache.org Delivered-To: mailing list commits@sis.apache.org Received: (qmail 77740 invoked by uid 99); 11 Apr 2020 21:18:24 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Sat, 11 Apr 2020 21:18:24 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 2EF0F8B6AA; Sat, 11 Apr 2020 21:18:23 +0000 (UTC) Date: Sat, 11 Apr 2020 21:18:26 +0000 To: "commits@sis.apache.org" Subject: [sis] 03/03: separate the Java2D-specific rendering code in MapCanvasAWT subclass. It allows to use MapCanvas base class with pure JavaFX graphics. MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit From: desruisseaux@apache.org In-Reply-To: <158663990312.26749.7200844545071852517@gitbox.apache.org> References: <158663990312.26749.7200844545071852517@gitbox.apache.org> X-Git-Host: gitbox.apache.org X-Git-Repo: sis X-Git-Refname: refs/heads/geoapi-4.0 X-Git-Reftype: branch X-Git-Rev: f2f577180863a162b48bdc9563e1a80f80ce2d84 X-Git-NotificationType: diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated Message-Id: <20200411211824.2EF0F8B6AA@gitbox.apache.org> 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 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: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Keyboard actions
Key Action
Move view to the right
Move view to the left
Move view to the top
Move view to the bottom
⎇ + ⇨ Rotate clockwise
⎇ + ⇦ Rotate anticlockwise
Page down Zoom in
Page up Zoom out
Home {@linkplain #reset() Reset}
Ctrl + above Above actions as a smaller translation, zoom or rotation
+ * + *

Subclassing

+ * Implementations need to add at least one JavaFX node in the {@link #floatingPane} list of children. + * Map rendering involves the following steps: + * + *
    + *
  1. {@link #createRenderer()} is invoked in the JavaFX thread. That method shall take a snapshot + * of every information needed for performing the rendering in background.
  2. + *
  3. {@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.
  4. + *
  5. {@link Renderer#commit()} is invoked in the JavaFX thread. The nodes prepared by {@code render()} + * can be transferred to {@link #floatingPane} in that method.
  6. + *
* * @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. - * - *

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.

- */ - 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 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, etc.) can be added by subclasses into the {@link Pane#getChildren()} list. + * This pane is initially empty; subclasses should add nodes (canvas, images, shapes, texts, etc.) + * 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}. * *

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.

*/ 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, etc.) 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, etc.) 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, etc.) 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, etc.) + * 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. - * - *

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.

- * - *

Subclasses are encouraged to override this method and configure the following properties - * before to invoke {@code super.execute(task)}:

- *
    - *
  • {@linkplain Task#runningProperty()}.addListener(…)
  • - *
  • {@linkplain Task#setOnFailed Task.setOnFailed}(…)
  • - *
- * - * @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: + * + *
    + *
  1. {@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.
  2. + *
  3. {@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.
  4. + *
  5. {@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.
  6. + *
* * @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, etc.) 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 { - /** - * 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 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() { + /** 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 implements Callback, 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 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. + * + *

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.

+ * + *

Overriding

+ * 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)}: + *
    + *
  • {@linkplain Task#runningProperty()}.addListener(…)
  • + *
  • {@linkplain Task#setOnFailed Task.setOnFailed}(…)
  • + *
+ * + * @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. + * + *

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.

+ */ + 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 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: + * + * + * + * + * + * + * + * + *
Methods invoked during a map rendering process
Method Thread Remarks
{@link #createRenderer()} JavaFX thread
{@link #render()} Background thread
{@link #paint(Graphics2D)} Background thread May be invoked many times.
{@link #commit()} JavaFX thread
+ * + * @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. + * + *

The default implementation does nothing.

+ */ + @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, etc.) created by + * {@link #render()}. + * + *

The default implementation does nothing.

+ * + * @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. + * + *

There is two possible situations:

+ *
    + *
  • 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.
  • + *
  • 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.
  • + *
+ * + * 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 { + /** + * 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 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 implements Callback, 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 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