/*
 * 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.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.GraphicsConfiguration;
import java.awt.Font;
import java.awt.font.GlyphVector;
import java.awt.image.DataBufferInt;
import java.awt.image.RenderedImage;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import javafx.application.Platform;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelBuffer;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.util.Callback;
import org.apache.sis.coverage.grid.j2d.ColorModelFactory;
import org.apache.sis.system.Configuration;


/**
 * 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.4
 * @since   1.1
 */
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.
     */
    @Configuration
    private static final boolean NATIVE_ACCELERATION = false;

    /**
     * Size (in pixels) of the warning symbol to show if rendering fail.
     */
    private static final int WARNING_SIZE = 200;

    /**
     * The symbols to show on top of the image as a warning when rendering failed.
     */
    private static final String WARNING_TEXT = "\u26A0";

    /**
     * Default value of {@link #imageMargin}.
     */
    @Configuration
    private static final Insets DEFAULT_MARGIN = new Insets(SCROLL_EVENT_SIZE);

    /**
     * Number of additional pixels to paint on each sides of the image, outside the viewing area.
     * Computing a larger image reduces the black borders that user sees during translations or
     * during zoom out before the new image is repainted.
     */
    public final ObjectProperty<Insets> imageMargin;

    /**
     * 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>
     *
     * <h4>Restriction</h4>
     * Type is restricted to {@link BufferedImage#TYPE_INT_ARGB_PRE} or {@link BufferedImage#TYPE_4BYTE_ABGR_PRE}
     * because JavaFX {@link PixelBuffer} (stored in {@link #bufferWrapper}) accepts only those types.
     * We arbitrarily choose {@code TYPE_INT_ARGB_PRE}.
     */
    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.
     * Used for creating compatible {@link #doubleBuffer} before updating image content.
     * 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).
     *
     * <h4>Invariants</h4>
     * <ul>
     *   <li>Shall be non-null if and only If {@link #buffer} is non-null.</li>
     * </ul>
     */
    private PixelBuffer<IntBuffer> bufferWrapper;

    /**
     * The node where the rendered map will be shown. Its content is prepared in a background thread
     * by {@link Renderer}. Subclasses should not set the image content directly.
     */
    protected final ImageView image;

    /**
     * Whether {@link #WARNING_TEXT} has been drawn on top of the image.
     * When warning is shown, navigation should be disabled.
     */
    private boolean isWarningShown;

    /**
     * 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);
        imageMargin = new SimpleObjectProperty<>(this, "imageMargin", DEFAULT_MARGIN);
        image = new ImageView();
        image.setPreserveRatio(true);
        floatingPane.getChildren().add(image);
    }

    /**
     * Returns the image bounds. This is used for determining if a
     * repaint is necessary after {@link MapCanvas} size changed.
     */
    @Override
    final Bounds getBoundsInParent() {
        return image.getBoundsInParent();
    }

    /**
     * Clears {@link #buffer} and all support fields.
     */
    private void clearBuffer() {
        buffer              = null;
        doubleBuffer        = null;
        bufferWrapper       = null;
        bufferConfiguration = null;
    }

    /**
     * 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.
     * 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>Collects all needed information.</td></tr>
     *   <tr><td>{@link #render()}</td>          <td>Background thread</td> <td>Computes what can be done in advance.</td></tr>
     *   <tr><td>{@link #paint(Graphics2D)}</td> <td>Background thread</td> <td>Holds a {@link Graphics2D}.</td></tr>
     *   <tr><td>{@link #commit(MapCanvas)}</td> <td>JavaFX thread</td>     <td>Saves data to cache for reuse.</td></tr>
     * </table>
     *
     * This class should not access any {@link MapCanvasAWT} property from a method invoked in background thread
     * ({@link #render()} and {@link #paint(Graphics2D)}). It may access {@link MapCanvasAWT} properties from the
     * {@link #commit(MapCanvas)} method.
     *
     * @author  Martin Desruisseaux (Geomatys)
     * @version 1.1
     * @since   1.1
     */
    protected abstract static class Renderer extends MapCanvas.Renderer {
        /**
         * Values of the {@link MapCanvasAWT#imageMargin} property at construction time.
         * Those values are initialized by {@link #isValid(Insets, BufferedImage)}.
         */
        private int left, top;

        /**
         * Image width and height, taking in account the margins.
         * Those values are initialized by {@link #isValid(Insets, BufferedImage)}.
         */
        private int width, height;

        /**
         * 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() {
        }

        /**
         * Rounds and clamp the given value. The upper limit is arbitrary.
         */
        private static int clamp(final double value) {
            return (int) Math.max(0, Math.min(Short.MAX_VALUE, Math.round(value)));
        }

        /**
         * Returns whether the given buffer is non-null and has the expected size.
         * This verification shall be done only after {@link #initialize(Pane)} has been invoked.
         *
         * @param  margin  value of {@link #imageMargin}.
         * @param  buffer  value of {@link #buffer}.
         */
        private boolean isValid(final Insets margin, final BufferedImage buffer) {
            width  = getWidth();
            height = getHeight();
            if (margin != null) {
                final int right, bottom;
                top    = clamp(margin.getTop());
                right  = clamp(margin.getRight());
                bottom = clamp(margin.getBottom());
                left   = clamp(margin.getLeft());
                width  = Math.addExact(width, left + right);
                height = Math.addExact(height, top + bottom);
            }
            return (buffer != null)
                    && buffer.getWidth()  == width
                    && buffer.getHeight() == height;
        }

        /**
         * Applies translation on the given graphics before {@link #paint(Graphics2D)}.
         */
        private void translate(final Graphics2D gr) {
            gr.translate(left, top);
        }

        /**
         * Compensates the translation applied by {@link #translate(Graphics2D)}.
         * This method is invoked only if the image painting has been successful,
         * otherwise we assume that old content is still present and require the
         * old translations.
         */
        private void translate(final ImageView image) {
            image.setTranslateX(-left);
            image.setTranslateY(-top);
        }

        /**
         * 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>
         *
         * @throws Exception if an error occurred while preparing data.
         */
        @Override
        protected void render() throws Exception {
        }

        /**
         * Invoked after {@link #render()} for doing the actual map painting.
         * This method is invoked in a background thread, potentially many times if {@link VolatileImage} content
         * is invalidated in the middle of rendering process. This method should not access any {@link MapCanvas}
         * property; if some canvas properties are needed, they should have been copied at construction time.
         *
         * @param  gr  the Java2D handler to use for rendering the map.
         */
        protected abstract void paint(Graphics2D gr);

        /**
         * Invoked in JavaFX thread after successful {@link #paint(Graphics2D)} completion. This method can update the
         * {@link #floatingPane} children with the nodes (images, shaped, <i>etc.</i>) created by {@link #render()}.
         * If this method detects that data has changed during the time {@code Renderer} was working in background,
         * then this method can return {@code true} for requesting a new repaint. In such case that repaint will use
         * a new {@link Renderer} instance; the current instance will not be reused.
         *
         * <p>The default implementation does nothing and returns {@code true}.</p>
         *
         * @param  canvas  the canvas where drawing has been done. It will be a {@link MapCanvasAWT} instance.
         * @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(MapCanvas canvas) {
            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 be 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>Otherwise (current buffer it still valid), we should not update {@link BufferedImage} in a 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. A {@link VolatileImage} is used
     *     as a temporary buffer.</li>
     * </ul>
     *
     * In all 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.
     *
     * <h4>Preconditions</h4>
     * It is important that no other worker is in progress at the time this method is invoked
     * ({@code assert renderingInProgress == null}), otherwise conflicts may happen when workers
     * will update the {@code MapCanvasAWT} fields after they completed their task.
     *
     * @see #requestRepaint()
     */
    @Override
    final RenderingTask<?> createWorker(final MapCanvas.Renderer mc) {
        assert Platform.isFxApplicationThread();
        final Renderer context = (Renderer) mc;
        if (!context.isValid(imageMargin.get(), buffer)) {
            clearBuffer();
            return new Creator(context);
        } else {
            return new Updater(context);
        }
    }

    /**
     * Background tasks for creating a new {@link BufferedImage}. This task is invoked when there are 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 RenderingTask<WritableImage> {
        /**
         * The user-provided object which will perform the actual rendering.
         * Its {@link Renderer#paint(Graphics2D)} method will be invoked in background thread.
         */
        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() throws Exception {
            renderer.render();
            final int width  = renderer.width;
            final int height = renderer.height;
            drawTo = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
            final Graphics2D gr = drawTo.createGraphics();
            try {
                configuration = gr.getDeviceConfiguration();
                renderer.translate(gr);
                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());
            renderer.translate(image);
            buffer              = drawTo;
            bufferWrapper       = wrapper;
            bufferConfiguration = configuration;
            final boolean done  = renderer.commit(MapCanvasAWT.this);
            clearWarning();
            renderingCompleted(this);
            if (!done || contentsChanged()) {
                repaint();
            }
        }

        @Override protected void failed()    {renderingCompleted(this);}
        @Override protected void cancelled() {renderingCompleted(this);}
    }

    /**
     * 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 RenderingTask<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 in background thread.
         */
        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() throws Exception {
            renderer.render();
            final int width  = renderer.width;
            final int height = renderer.height;
            VolatileImage drawTo = previousBuffer;
            previousBuffer = null;                      // For letting GC do its work.
            if (drawTo == null) {
                drawTo = configuration.createCompatibleVolatileImage(width, height, VolatileImage.TRANSLUCENT);
            }
            boolean invalid = true;
            try {
                do {
                    if (drawTo.validate(configuration) == VolatileImage.IMAGE_INCOMPATIBLE) {
                        drawTo = configuration.createCompatibleVolatileImage(width, height, VolatileImage.TRANSLUCENT);
                    }
                    final Graphics2D gr = drawTo.createGraphics();
                    try {
                        gr.setBackground(ColorModelFactory.TRANSPARENT);
                        gr.clearRect(0, 0, drawTo.getWidth(), drawTo.getHeight());
                        renderer.translate(gr);
                        renderer.paint(gr);
                    } finally {
                        gr.dispose();
                    }
                    invalid = drawTo.contentsLost();
                } while (invalid && !isCancelled());
            } finally {
                if (invalid) {
                    drawTo.flush();         // Release native resources on cancellation or exception thrown.
                }
            }
            return drawTo;
        }

        /**
         * Invoked by {@link PixelBuffer#updateBuffer(Callback)} for updating the {@link #buffer} content.
         * This method must be invoked in JavaFX thread. It copies the {@link VolatileImage} content to the
         * {@link BufferedImage} shared with JavaFX in a single {@code Graphics2D.drawImage(…)} operation.
         * The whole destination surface shall be written by {@code drawImage(…)}, so there is no need to invoke
         * {@link Graphics2D#clearRect} first. It is important because the small delay between {@code clearRect(…)}
         * and {@code drawImage(…)} can cause twinkle.
         */
        @Override
        public Rectangle2D call(final PixelBuffer<IntBuffer> wrapper) {
            final VolatileImage drawTo = doubleBuffer;
            final Graphics2D gr = buffer.createGraphics();
            try {
                gr.setComposite(AlphaComposite.Src);        // Copy source (previous destination is discarded).
                gr.drawImage(drawTo, 0, 0, null);
                contentsLost = drawTo.contentsLost();
            } finally {
                gr.dispose();
            }
            return null;                                    // Indicate that the entire buffer was dirty.
        }

        /**
         * Invoked in JavaFX thread on success. The actual transfer from Java2D image to JavaFX image
         * will happen just before the next pulse for making sure that the affine transform and the
         * image are updated together before rendering. Doing that way avoid flickering effects.
         */
        @Override
        protected void succeeded() {
            final Scene scene = fixedPane.getScene();
            if (scene != null) {
                final Runnable pulseAction = new Runnable() {
                    @Override public void run() {
                        scene.removePreLayoutPulseListener(this);
                        transferImage();
                    }
                };
                scene.addPreLayoutPulseListener(pulseAction);
                Platform.requestNextPulse();
            } else {
                transferImage();
            }
        }

        /**
         * 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.
         */
        private void transferImage() {
            final VolatileImage drawTo = getValue();
            doubleBuffer = drawTo;
            try {
                bufferWrapper.updateBuffer(this);   // This will invoke the `call(PixelBuffer)` method above.
            } finally {
                drawTo.flush();                     // Release native resources.
            }
            renderer.translate(image);
            final boolean done = renderer.commit(MapCanvasAWT.this);
            clearWarning();
            renderingCompleted(this);
            if (!done || contentsLost || contentsChanged()) {
                repaint();
            }
        }

        /** Clears the image in the same way than failure. Defined for safety but should not happen. */
        @Override protected void cancelled() {failed();}

        /**
         * Invoked in JavaFX thread on failure. No result is available. The JavaFX image is set to an empty image.
         * {@link VolatileImage#flush()} has already been invoked by the finally block in {@link #call()}.
         */
        @Override
        protected void failed() {
            if (!isWarningShown) {
                final int rw = renderer.getWidth();
                final int rh = renderer.getHeight();
                final int x = Math.max((rw - WARNING_SIZE) / 2, 0);
                final int y = Math.max((rh - WARNING_SIZE) / 2, 0);
                final int width  = rw - 2*x;
                final int height = rh - 2*y;
                final Font font = new Font(Font.SERIF, Font.PLAIN, height);
                bufferWrapper.updateBuffer((final PixelBuffer<IntBuffer> wrapper) -> {
                    final Graphics2D gr = buffer.createGraphics();
                    try {
                        renderer.translate(gr);
                        gr.setColor(new Color(0xA0804040, true));
                        gr.fillRoundRect(x, y, width, height, width/5, height/5);
                        gr.setColor(Color.RED);
                        final GlyphVector glyphs = font.createGlyphVector(gr.getFontRenderContext(), WARNING_TEXT);
                        final java.awt.geom.Rectangle2D vb = glyphs.getVisualBounds();
                        gr.drawGlyphVector(glyphs, (float) (x + 0.5*width  - vb.getCenterX()),
                                                   (float) (y + 0.5*height - vb.getCenterY()));
                    } finally {
                        gr.dispose();
                    }
                    return null;
                });
                isWarningShown = true;
                setNavigationDisabled(true);
            }
            renderingCompleted(this);
        }
    }

    /**
     * If warning symbol was drawn on top of the image, clears it and restores navigation.
     */
    private void clearWarning() {
        if (isWarningShown) {
            isWarningShown = false;
            setNavigationDisabled(false);
        }
    }

    /**
     * Clears the image and all intermediate buffer.
     * Invoking this method may help to release memory when the map is no longer shown.
     *
     * <h4>Usage</h4>
     * Overriding methods in subclasses should invoke {@code super.clear()}.
     * Other methods should generally not invoke this method directly,
     * and use the following code instead:
     *
     * {@snippet lang="java" :
     *     runAfterRendering(this::clear);
     *     }
     *
     * @see #runAfterRendering(Runnable)
     */
    @Override
    protected void clear() {
        image.setImage(null);
        clearBuffer();
        super.clear();
    }
}
