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 8ceffa01786c878f81c5420428990d58012ac5fe
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sun Nov 28 12:14:46 2021 +0100

    Better rendering of `ImagePropertyExplorer` content when the property value 
is another image.
---
 .../sis/gui/coverage/ImagePropertyExplorer.java    |   2 +-
 .../apache/sis/internal/gui/ImageConverter.java    | 137 +++++++++++++++------
 .../org/apache/sis/internal/gui/PropertyView.java  |  62 +++++++---
 .../sis/internal/map/coverage/RenderingData.java   |   2 +
 4 files changed, 147 insertions(+), 56 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
index 39e72dc..eb1208d 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
@@ -530,7 +530,7 @@ public class ImagePropertyExplorer extends Widget {
 
     /**
      * Invoked when an image is selected in the tree of image sources. The 
selected image
-     * is not necessarily {@link #image} property value; it may be one of its 
sources.
+     * is not necessarily the {@link #image} property value; it may be one of 
its sources.
      * If no image is explicitly selected, defaults to the root image.
      */
     private void imageSelected(final RenderedImage selected) {
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
index a407d7f..5c96296 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
@@ -26,6 +26,8 @@ import java.awt.image.BufferedImage;
 import java.awt.image.DataBufferInt;
 import java.awt.image.RenderedImage;
 import javafx.concurrent.Task;
+import javafx.scene.layout.Region;
+import javafx.scene.image.Image;
 import javafx.scene.image.ImageView;
 import javafx.scene.image.PixelFormat;
 import javafx.scene.image.PixelWriter;
@@ -35,6 +37,7 @@ import org.apache.sis.image.PlanarImage;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.jdk9.JDK9;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.measure.NumberRange;
@@ -46,26 +49,23 @@ import org.apache.sis.math.Statistics;
  * This task should be used only for small images (thumbnail) because some 
potentially costly resources
  * are created each time.
  *
+ * <p>The {@link ImageView} should not be modified by caller. Instead, the 
{@link #clear(ImageView)}
+ * method should be invoked for setting the image to null, because this class 
also manages properties
+ * associated to the {@link ImageView}.</p>
+ *
  * <p>Current implementation returns statistics on sample values as a 
side-product.
- * It may change in any future version.</p>
+ * The statistics may be null if they were not computed.
+ * This policy may change in any future version.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
 final class ImageConverter extends Task<Statistics[]> {
     /**
-     * The maximal image width and height, as an arbitrary value that may 
change in any future version.
-     * This size should be a typical size expected for the JavaFX control 
showing the image.
-     * If the {@link ImageView} is larger than this size, the image will be 
rescaled by JavaFX.
-     * The quality is lower than if we had let the "resample" operation do its 
work at full resolution,
-     * but {@link PropertyView} should be used only of overview anyway.
-     */
-    private static final int MAX_SIZE = 600;
-
-    /**
      * Colors to apply on the mask image when that image is overlay on top of 
another image.
+     * Current value is a transparent yellow color.
      */
     private static final Map<NumberRange<?>,Color[]> MASK_TRANSPARENCY = 
JDK9.mapOf(
             NumberRange.create(0, true, 0, true), new Color[] 
{ColorModelFactory.TRANSPARENT},
@@ -79,18 +79,23 @@ final class ImageConverter extends Task<Statistics[]> {
     /**
      * Pixel coordinates of the region to render, or {@code null} for the 
whole image.
      */
-    private Rectangle bounds;
+    private Rectangle sourceAOI;
 
     /**
-     * Size of the image actually rendered. This is the {@link #bounds} size,
-     * unless that size is greater than {@link #MAX_SIZE} in which case it is 
scaled down.
+     * On input, the canvas size. On output, the actual image size rendered in 
the canvas.
+     * One of the width or height may be modified on output for preserving 
image proportions.
      */
     private int width, height;
 
     /**
+     * Value to give to {@link ImageView} x/y properties after the image has 
been created.
+     */
+    private int xpos, ypos;
+
+    /**
      * Where to write the image. This will be updated in JavaFX thread.
      */
-    private final ImageView canvas;
+    private final ImageView view;
 
     /**
      * The ARGB values to be copied in the JavaFX image.
@@ -100,38 +105,85 @@ final class ImageConverter extends Task<Statistics[]> {
     /**
      * Creates a new task for converting the given image.
      */
-    ImageConverter(final RenderedImage source, final Rectangle bounds, final 
ImageView canvas) {
-        this.source = source;
-        this.bounds = bounds;
-        this.canvas = canvas;
+    ImageConverter(final RenderedImage source, final Rectangle sourceAOI, 
final ImageView view, final Region canvas) {
+        this.source    = source;
+        this.sourceAOI = sourceAOI;
+        this.view      = view;
+        this.width     = Numerics.clamp(Math.round(canvas.getWidth()));
+        this.height    = Numerics.clamp(Math.round(canvas.getHeight()));
+    }
+
+    /**
+     * Sets the image property to {@code null} on the given view.
+     * This method also clears other properties managed by {@link 
ImageConverter}.
+     *
+     * @param  view  the view on which to set the image property to {@code 
null}.
+     */
+    public static void clear(final ImageView view) {
+        view.setImage(null);
+        view.setUserData(null);
+    }
+
+    /**
+     * Returns {@code true} if the {@link #call()} method needs to be invoked 
in a background thread.
+     * This method may return {@code false} because the canvas size is zero, 
or because current content can be reused.
+     * The content can be reused if the canvas size did not changed, or 
changed in a way that does not require repaint
+     * (i.e. if the size that changed is the size that was already too wide 
for fitting the image inside the canvas).
+     *
+     * @param  newAOI  whether {@link #sourceAOI} had a different value during 
previous rendering.
+     * @return whether this task needs to be run.
+     */
+    public boolean needsRun(final boolean newAOI) {
+        if (width <= 0 || height <= 0) {
+            return false;
+        }
+        if (!newAOI && view.getUserData() == source) {
+            final Image image = view.getImage();
+            final double dx = width  - image.getWidth();
+            final double dy = height - image.getHeight();
+            /*
+             * Conditions: the image is not larger than the canvas (dx and dy 
>= 0)
+             * and at least one image side has the maximal size (dx or dy == 
0).
+             */
+            if (dx >= 0 && dy >= 0 && (dx == 0 || dy == 0)) {
+                view.setX(dx / 2);
+                view.setY(dy / 2);
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
      * Prepares the ARGB values to be written in the JavaFX image.
      * The task opportunistically returns statistics on all bands of the 
source image.
-     * Those statistics were used for stretching the color ramp before to 
paint the JavaFX image.
+     * They are the statistics used for stretching the color ramp before to 
paint the JavaFX image.
+     *
+     * @return statistics on sample value, or {@code null} if not computed.
      */
     @Override
     protected Statistics[] call() {
-        if (bounds == null) {
-            bounds = ImageUtilities.getBounds(source);
+        if (sourceAOI == null) {
+            sourceAOI = ImageUtilities.getBounds(source);
         }
-        width  = Math.min(bounds.width,  MAX_SIZE);
-        height = Math.min(bounds.height, MAX_SIZE);
-        final double scale = Math.max(width  / (double) bounds.width,
-                                      height / (double) bounds.height);
         /*
          * Use a uniform scale. At least one of `width` or `height` will be 
unchanged.
+         * We favor showing fully the image instead of filling all the canvas 
space.
          */
-        width  = (int) Math.round(scale * bounds.width);
-        height = (int) Math.round(scale * bounds.height);
+        final double scale = Math.min(width  / (double) sourceAOI.width,
+                                      height / (double) sourceAOI.height);
+        xpos = (width  - (width  = Numerics.clamp(Math.round(scale * 
sourceAOI.width )))) / 2;
+        ypos = (height - (height = Numerics.clamp(Math.round(scale * 
sourceAOI.height)))) / 2;
+        if (width <= 0 || height <= 0) {
+            return null;
+        }
         final AffineTransform toCanvas = 
AffineTransform.getScaleInstance(scale, scale);
-        toCanvas.translate(-bounds.x, -bounds.y);
+        toCanvas.translate(-sourceAOI.x, -sourceAOI.y);
         /*
-         * Stretch color ramp using statistics on the source image before to 
pain on JavaFX image.
+         * Stretch color ramp using statistics about source image before to 
paint on JavaFX image.
          */
         final ImageProcessor processor  = new ImageProcessor();
-        final Statistics[]   statistics = processor.valueOfStatistics(source, 
bounds, (DoubleUnaryOperator[]) null);
+        final Statistics[]   statistics = processor.valueOfStatistics(source, 
sourceAOI, (DoubleUnaryOperator[]) null);
         final RenderedImage  image      = processor.stretchColorRamp(source, 
JDK9.mapOf("multStdDev", 3, "statistics", statistics));
         final RenderedImage  mask       = getMask(processor);
         final BufferedImage  buffer     = new BufferedImage(width, height, 
BufferedImage.TYPE_INT_ARGB_PRE);
@@ -168,16 +220,20 @@ final class ImageConverter extends Task<Statistics[]> {
      */
     @Override
     protected void succeeded() {
-        WritableImage destination = (WritableImage) canvas.getImage();
-        if (destination == null || destination.getWidth() != width || 
destination.getHeight() != height) {
-            destination = new WritableImage(width, height);
+        WritableImage destination = null;
+        if (data != null) {
+            destination = (WritableImage) view.getImage();
+            if (destination == null || destination.getWidth() != width || 
destination.getHeight() != height) {
+                destination = new WritableImage(width, height);
+            }
+            final PixelWriter writer = destination.getPixelWriter();
+            writer.setPixels(0, 0, width, height, 
PixelFormat.getIntArgbPreInstance(), data, 0, width);
+            data = null;
         }
-        final PixelWriter writer = destination.getPixelWriter();
-        writer.setPixels(0, 0, width, height, 
PixelFormat.getIntArgbPreInstance(), data, 0, width);
-        data = null;
-        canvas.setImage(destination);
-        canvas.setFitWidth (bounds.width);
-        canvas.setFitHeight(bounds.height);
+        view.setImage(destination);
+        view.setUserData(source);
+        view.setX(xpos);
+        view.setY(ypos);
     }
 
     /**
@@ -186,6 +242,7 @@ final class ImageConverter extends Task<Statistics[]> {
     @Override
     protected void failed() {
         data = null;
+        clear(view);
     }
 
     /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
index 50879d1..a57ff74 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
@@ -29,6 +29,8 @@ import java.text.ParsePosition;
 import java.text.ParseException;
 import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
 import javafx.beans.property.ObjectProperty;
 import javafx.concurrent.Task;
 import javafx.geometry.Insets;
@@ -52,7 +54,8 @@ import org.apache.sis.util.resources.Vocabulary;
  * A viewer for property value. The property may be of various class (array, 
image, <i>etc</i>).
  * If the type is unrecognized, the property is shown as text.
  *
- * <p>This class extends {@link CompoundFormat} for implementation convenience 
only.</p>
+ * <p>This class extends {@link CompoundFormat} and implements {@code 
ChangeListener} for
+ * implementation convenience only. Users should not rely on this 
implementation details.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
@@ -60,7 +63,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * @module
  */
 @SuppressWarnings({"serial","CloneableImplementsClone"})            // Not 
intended to be serialized.
-public final class PropertyView extends CompoundFormat<Object> {
+public final class PropertyView extends CompoundFormat<Object> implements 
ChangeListener<Number> {
     /**
      * The current property value. This is used for detecting changes.
      */
@@ -138,6 +141,8 @@ public final class PropertyView extends 
CompoundFormat<Object> {
         if (background != null) {
             imageCanvas.backgroundProperty().bind(background);
         }
+        imageCanvas.widthProperty() .addListener(this);
+        imageCanvas.heightProperty().addListener(this);
     }
 
     /**
@@ -210,7 +215,8 @@ public final class PropertyView extends 
CompoundFormat<Object> {
      * @param  visibleBounds  if the property is an image, currently visible 
region. Can be {@code null}.
      */
     public void set(final Object newValue, final Rectangle visibleBounds) {
-        if (newValue != value || !Objects.equals(visibleBounds, 
visibleImageBounds)) {
+        final boolean boundsChanged = !Objects.equals(visibleBounds, 
visibleImageBounds);
+        if (newValue != value || boundsChanged) {
             if (runningTask != null) {
                 runningTask.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO);
                 runningTask = null;
@@ -220,7 +226,7 @@ public final class PropertyView extends 
CompoundFormat<Object> {
             if (newValue == null) {
                 content = null;
             } else if (newValue instanceof RenderedImage) {
-                content = setImage((RenderedImage) newValue);
+                content = setImage((RenderedImage) newValue, boundsChanged);
             } else if (newValue instanceof Throwable) {
                 content = setText((Throwable) newValue);
             } else if (newValue instanceof Collection<?>) {
@@ -278,8 +284,11 @@ public final class PropertyView extends 
CompoundFormat<Object> {
 
     /**
      * Sets the property value to the given image.
+     *
+     * @param  image  the property value to set, or {@code null}.
+     * @param  boundsChanged  whether {@link #visibleImageBounds} changed 
since last call.
      */
-    private Node setImage(final RenderedImage image) {
+    private Node setImage(final RenderedImage image, final boolean 
boundsChanged) {
         ImageView node = imageView;
         if (node == null) {
             node = new ImageView();
@@ -310,14 +319,16 @@ public final class PropertyView extends 
CompoundFormat<Object> {
             imagePane.setHgap(0);
             imageView = node;
         }
-        final ImageConverter converter = new ImageConverter(image, 
visibleImageBounds, node);
-        converter.setOnSucceeded((e) -> taskCompleted(converter.getValue()));
-        converter.setOnFailed((e) -> {
-            taskCompleted(null);
-            view.set(setText(e.getSource().getException()));
-        });
-        runningTask = converter;
-        BackgroundThreads.execute(converter);
+        final ImageConverter converter = new ImageConverter(image, 
visibleImageBounds, node, imageCanvas);
+        if (converter.needsRun(boundsChanged)) {
+            converter.setOnSucceeded((e) -> 
taskCompleted(converter.getValue()));
+            converter.setOnFailed((e) -> {
+                taskCompleted(null);
+                view.set(setText(e.getSource().getException()));
+            });
+            runningTask = converter;
+            BackgroundThreads.execute(converter);
+        }
         return imagePane;
     }
 
@@ -349,6 +360,23 @@ public final class PropertyView extends 
CompoundFormat<Object> {
         }
         sampleValueRange.setText(range);
         meanValue.setText(mean);
+        changed(null, null, null);      // For centering the image.
+    }
+
+    /**
+     * Invoked when the image canvas size changed. If the previous canvas size 
was 0, the image could not be rendered
+     * during the previous call to {@link #setImage(RenderedImage, boolean)}, 
so we need to call that method again now
+     * that the image size is known. In addition we also need to move the 
image to canvas center.
+     *
+     * @param  property  the image canvas property that changed (width or 
height).
+     * @param  oldValue  the old width or height value.
+     * @param  newValue  the new width or height value.
+     */
+    @Override
+    public void changed(final ObservableValue<? extends Number> property, 
final Number oldValue, final Number newValue) {
+        if (value instanceof RenderedImage) {
+            setImage((RenderedImage) value, false);
+        }
     }
 
     /**
@@ -357,7 +385,11 @@ public final class PropertyView extends 
CompoundFormat<Object> {
     public void clear() {
         value = null;
         view.set(null);
-        if (textView  != null) textView .setText (null);
-        if (imageView != null) imageView.setImage(null);
+        if (textView != null) {
+            textView .setText (null);
+        }
+        if (imageView != null) {
+            ImageConverter.clear(imageView);
+        }
     }
 }
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index a4bc23a..6cba9de 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -93,6 +93,8 @@ import org.apache.sis.portrayal.PlanarCanvas;       // For 
javadoc.
  * in pixel coordinates, the translation terms in {@code resampledToDisplay} 
transform should stay integers.
  *
  * <p>Current version of this class does not perform a special case for {@link 
BufferedImage}.
+ * It may not be desirable because interpolations would not be applied in the 
same way, except
+ * when SIS {@link ImageProcessor} would have interpolated RGB color values 
anyway like Java2D.
  * We wait to see if this class works well in the general case before doing 
special cases.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)

Reply via email to