sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Display values of "positional errors" operation on status bar.
Date Sat, 13 Jun 2020 16:16:06 GMT
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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new acc22f8  Display values of "positional errors" operation on status bar.
acc22f8 is described below

commit acc22f84750016b070066e4514c8ff246365fa52
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Jun 13 18:14:34 2020 +0200

    Display values of "positional errors" operation on status bar.
---
 .../org/apache/sis/gui/coverage/CellFormat.java    |  44 ++---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  44 ++++-
 .../apache/sis/gui/coverage/CoverageControls.java  |   2 +
 .../apache/sis/gui/coverage/ImageOperation.java    |   9 +-
 .../org/apache/sis/gui/coverage/RenderingData.java |   6 +-
 .../apache/sis/gui/coverage/StatusBarSupport.java  | 219 +++++++++++++++++++++
 .../java/org/apache/sis/gui/map/StatusBar.java     |  14 +-
 .../org/apache/sis/gui/map/ValuesUnderCursor.java  |  35 +++-
 .../org/apache/sis/image/PositionalErrorImage.java |  33 ++++
 .../java/org/apache/sis/image/ResampledImage.java  |  13 +-
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  27 +++
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 14 files changed, 404 insertions(+), 49 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
index 6270884..13a9f60 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.gui.coverage;
 
-import java.lang.reflect.Array;
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
 import java.text.FieldPosition;
@@ -27,12 +26,10 @@ import javafx.scene.control.ComboBox;
 import javafx.scene.control.Tooltip;
 import javafx.scene.layout.Background;
 import javafx.util.Duration;
-import org.apache.sis.image.PlanarImage;
-import org.apache.sis.math.DecimalFunctions;
-import org.apache.sis.util.Numbers;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.gui.RecentChoices;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 
 
 /**
@@ -234,33 +231,22 @@ final class CellFormat extends SimpleStringProperty {
         if (dataTypeisInteger) {
             cellFormat.setMaximumFractionDigits(0);
         } else {
-            int n = 1;          // Default value if we can not determine the number of fraction
digits.
-            final Object property = image.getProperty(PlanarImage.SAMPLE_RESOLUTIONS_KEY);
-            if (property != null) {
-                final int c = Numbers.getEnumConstant(property.getClass().getComponentType());
-                if (c >= Numbers.BYTE && c <= Numbers.BIG_DECIMAL &&
band < Array.getLength(property)) {
-                    final double resolution = Math.abs(((Number) Array.get(property, band)).doubleValue());
-                    if (resolution > 0 && resolution <= Double.MAX_VALUE) {
    // Non-zero, non-NaN and finite.
-                        n = DecimalFunctions.fractionDigitsForDelta(resolution, false);
-                        if (n > 6 || n < -9) {      // Arbitrary threshold for switching
to scientific notation.
-                            if (cellFormat instanceof DecimalFormat) {
-                                if (classicFormatPattern == null) {
-                                    final DecimalFormat df = (DecimalFormat) cellFormat;
-                                    classicFormatPattern = df.toPattern();
-                                    df.applyPattern("0.###E00");
-                                }
-                                n = 3;
-                            }
-                        } else if (classicFormatPattern != null) {
-                            ((DecimalFormat) cellFormat).applyPattern(classicFormatPattern);
-                            classicFormatPattern = null;
-                        }
-                        if (n < 0) n = 0;
+            ImageUtilities.getFractionDigits(image, band).ifPresent((n) -> {
+                if (n > 6 || n < -9) {      // Arbitrary threshold for switching to
scientific notation.
+                    if (cellFormat instanceof DecimalFormat && classicFormatPattern
== null) {
+                        final DecimalFormat df = (DecimalFormat) cellFormat;
+                        classicFormatPattern = df.toPattern();
+                        df.applyPattern("0.000E00");
+                        return;
                     }
+                } else if (classicFormatPattern != null) {
+                    ((DecimalFormat) cellFormat).applyPattern(classicFormatPattern);
+                    classicFormatPattern = null;
                 }
-            }
-            cellFormat.setMinimumFractionDigits(n);
-            cellFormat.setMaximumFractionDigits(n);
+                if (n < 0) n = 0;
+                cellFormat.setMinimumFractionDigits(n);
+                cellFormat.setMaximumFractionDigits(n);
+            });
         }
         buffer.setLength(0);
         formatCell(lastValue);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index b44f104..e4ded7e 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -46,6 +46,7 @@ import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.Interpolation;
+import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.gui.map.MapCanvasAWT;
 import org.apache.sis.internal.gui.Resources;
@@ -110,6 +111,17 @@ public class CoverageCanvas extends MapCanvasAWT {
     private final Map<ImageDerivative,RenderedImage> resampledImages;
 
     /**
+     * Helper methods for configuring the {@link StatusBar} when the user selects an operation.
+     * This is non-null only if this {@link CoverageCanvas} is used together with a status
bar.
+     *
+     * <p>Consider as final after {@link #initialize(StatusBar)} invocation. This field
may be removed in a future
+     * version if we define a good public API for coverage operations in replacement of {@link
ImageOperation}.</p>
+     *
+     * @see #initialize(StatusBar)
+     */
+    private StatusBarSupport statusBar;
+
+    /**
      * Creates a new two-dimensional canvas for {@link RenderedImage}.
      */
     public CoverageCanvas() {
@@ -125,6 +137,17 @@ public class CoverageCanvas extends MapCanvasAWT {
     }
 
     /**
+     * Completes initialization of this canvas for use with the given status bar.
+     * The intent is to be notified when an {@link ImageOperation} is applied on the coverage.
+     * We do not yet have a public API for managing the display of image operations in a
canvas.
+     * This method may be removed in a future SIS version if we have a clear API for creating
a
+     * new {@link GridCoverage} from the result of an image operation.
+     */
+    final void initialize(final StatusBar bar) {
+        statusBar = new StatusBarSupport(bar);
+    }
+
+    /**
      * Returns the region containing the image view.
      * The subclass is implementation dependent and may change in any future version.
      *
@@ -255,7 +278,7 @@ public class CoverageCanvas extends MapCanvasAWT {
                 }
 
                 /**
-                 * Invoked in JavaFX thread for setting the image to the instance we juste
fetched.
+                 * Invoked in JavaFX thread for setting the image to the instance we just
fetched.
                  */
                 @Override protected void succeeded() {
                     setRawImage(getValue(), imageGeometry);
@@ -422,14 +445,28 @@ public class CoverageCanvas extends MapCanvasAWT {
         final RenderedImage newValue = worker.resampledImage;
         final RenderedImage oldValue = resampledImages.put(ImageDerivative.NONE, newValue);
         if (oldValue != newValue && oldValue != null) {
+            /*
+             * If resampled image changed, then all derivative images (with stretched color
ramp
+             * or other operation applied) are not valid anymore. We need to empty the cache.
+             */
             resampledImages.clear();
             resampledImages.put(ImageDerivative.NONE, newValue);
         }
         resampledImages.put(data.selectedDerivative, worker.filteredImage);
+        /*
+         * If an operation has been applied, we may need to update the object used for computing
+         * the sample values shown on the status bar.
+         */
+        if (statusBar != null) {
+            statusBar.select(data.selectedDerivative.operation, worker.filteredImage);
+        }
     }
 
     /**
-     * Invoked when the user selected a new operation.
+     * Invoked by {@link CoverageControls} when the user selected a new operation.
+     * Execution of an image operation may produce sample values very different than the
original ones.
+     * This change may require to update {@link StatusBar#sampleValuesProvider} accordingly.
+     * This update is applied by {@link #cacheRenderingData(Worker)}.
      */
     final void setOperation(final ImageOperation selection) {
         data.selectedDerivative = data.selectedDerivative.setOperation(selection);
@@ -437,7 +474,8 @@ public class CoverageCanvas extends MapCanvasAWT {
     }
 
     /**
-     * Invoked when the user selected a new color stretching mode.
+     * Invoked by {@link CoverageControls} when the user selected a new color stretching
mode.
+     * The sample values are assumed the same; only the image appearance is modified.
      */
     final void setStyling(final Stretching selection) {
         data.selectedDerivative = data.selectedDerivative.setStyling(selection);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index 13672c4..4b0701c 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -85,6 +85,7 @@ final class CoverageControls extends Controls implements PropertyChangeListener
         view = new CoverageCanvas();
         view.setBackground(background);
         final StatusBar statusBar = new StatusBar(view, referenceSystems);
+        view.initialize(statusBar);
         imageAndStatus = new BorderPane(view.getView());
         imageAndStatus.setBottom(statusBar.getView());
         /*
@@ -230,6 +231,7 @@ final class CoverageControls extends Controls implements PropertyChangeListener
 
     /**
      * Invoked when a canvas property changed.
+     * The property of interest is {@value CoverageCanvas#OBJECTIVE_CRS_PROPERTY}.
      */
     @Override
     public void propertyChange(final PropertyChangeEvent event) {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageOperation.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageOperation.java
index 9faba4e..eb2ae46 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageOperation.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageOperation.java
@@ -30,6 +30,10 @@ import static org.apache.sis.image.ResampledImage.POSITIONAL_ERRORS_KEY;
  * Predefined operations that can be applied on image.
  * The resulting images use the same coordinate system than the original image.
  *
+ * <p>This class may be temporary. We need a better way to handle replacement of original
image by result of image
+ * operations. A difficulty is to recreate a full {@code GridCoverage} from an image, in
particular for the result
+ * of image derived from resampled image (because the original grid geometry is no longer
valid).</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
@@ -47,12 +51,12 @@ enum ImageOperation {
     POSITIONAL_ERROR(Resources.format(Resources.Keys.PositionalErrors)) {
         @Override final RenderedImage apply(final RenderedImage source) {
             final Object value = source.getProperty(POSITIONAL_ERRORS_KEY);
-            return (value instanceof RenderedImage) ? (RenderedImage) value : null;
+            return (value instanceof RenderedImage) ? (RenderedImage) value : source;
         }
     };
 
     /**
-     * The label to show in menu.
+     * The label to show in the list view.
      */
     private final String label;
 
@@ -77,6 +81,7 @@ enum ImageOperation {
 
     /**
      * Applies the operation on given image.
+     * If the operation can not be applied, then {@code source} is returned as-is.
      */
     RenderedImage apply(final RenderedImage source) {
         return source;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index f7a38d0..e787dfa 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -252,7 +252,7 @@ final class RenderingData implements Cloneable {
          *
          * TODO: if user pans the image close to integer range limit, we should create a
new resampled image
          *       shifted to new location (i.e. clear `CoverageCanvas.resampledImages` for
forcing this method
-         *       to be invoked again).
+         *       to be invoked again). The intent is to move away from integer overflow situation.
          */
         final LinearTransform inverse = objectiveToDisplay.inverse();
         displayToObjective = AffineTransforms2D.castOrCopy(inverse);
@@ -271,7 +271,9 @@ final class RenderingData implements Cloneable {
      * @return image with operation applied and color ramp stretched. May be the same instance
than given image.
      */
     final RenderedImage filter(RenderedImage resampledImage) {
-        resampledImage = selectedDerivative.operation.apply(resampledImage);
+        if (resampledImage == (resampledImage = selectedDerivative.operation.apply(resampledImage)))
{
+            selectedDerivative = selectedDerivative.setOperation(ImageOperation.NONE);
+        }
         if (selectedDerivative.styling != Stretching.NONE) {
             final Map<String,Object> modifiers = new HashMap<>(4);
             /*
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StatusBarSupport.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StatusBarSupport.java
new file mode 100644
index 0000000..2622a04
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StatusBarSupport.java
@@ -0,0 +1,219 @@
+/*
+ * 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.coverage;
+
+import java.util.Map;
+import java.util.EnumMap;
+import java.awt.image.RenderedImage;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import javafx.beans.property.ObjectProperty;
+import javafx.scene.control.CheckMenuItem;
+import org.apache.sis.gui.map.StatusBar;
+import org.apache.sis.gui.map.ValuesUnderCursor;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.util.Workaround;
+import org.apache.sis.util.resources.Vocabulary;
+import org.opengis.geometry.DirectPosition;
+
+
+/**
+ * Methods for configuring the {@link StatusBar} associated to a {@link CoverageCanvas}.
+ * This is used for changing the {@link ValuesUnderCursor} instance used by the status bar
+ * when the canvas shows an {@link ImageDerivative} instead than the original image.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class StatusBarSupport {
+    /**
+     * The object to use for providing values under cursor. The default evaluator is the
one created by
+     * {@link ValuesUnderCursor#create(MapCanvas)}. If the image to display is the result
of an operation,
+     * then the default evaluator is temporarily replaced by an {@link ImageOperation}-specific
evaluator.
+     *
+     * @see StatusBar#sampleValuesProvider
+     */
+    private final ObjectProperty<ValuesUnderCursor> selectedProvider;
+
+    /**
+     * The objects to use for providing values under cursor in image computed by an operation.
+     * Evaluators are created when first needed and retained for {@link CoverageCanvas} lifetime.
+     * We need to keep those instances after creation in order to preserve user settings
in
+     * {@link Evaluator#valueChoices} menu items.
+     */
+    private final Map<ImageOperation,ValuesUnderCursor> sampleValuesProviders;
+
+    /**
+     * Creates a new support class for the given status bar.
+     */
+    StatusBarSupport(final StatusBar bar) {
+        selectedProvider = bar.sampleValuesProvider;
+        sampleValuesProviders = new EnumMap<>(ImageOperation.class);
+        sampleValuesProviders.put(ImageOperation.NONE, selectedProvider.get());
+    }
+
+    /**
+     * Notifies the status bar that a new operation is applied on the image.
+     * This method updates {@link StatusBar#sampleValuesProvider} with an instance
+     * appropriate for the image shown.
+     *
+     * @param  operation  the operation applied on the image.
+     * @param  image      the image resulting from the operation.
+     */
+    final void select(final ImageOperation operation, final RenderedImage image) {
+        ValuesUnderCursor evaluator = sampleValuesProviders.computeIfAbsent(operation, (o)
-> new Evaluator(image));
+        if (evaluator instanceof Evaluator) {
+            ((Evaluator) evaluator).data = image;
+        }
+        selectedProvider.set(evaluator);
+    }
+
+    /**
+     * Provides sample values for result of image operation.
+     * Current implementation assumes a single-banded image with a fixed number of fraction
digits.
+     */
+    private static final class Evaluator extends ValuesUnderCursor {
+        /**
+         * The image from which sample values will be read.
+         */
+        private RenderedImage data;
+
+        /**
+         * The object to use for formatting numbers.
+         */
+        private final NumberFormat format;
+
+        /**
+         * Dummy object recycled for each evaluation.
+         */
+        private final FieldPosition pos;
+
+        /**
+         * Where to format the number.
+         */
+        private final StringBuffer buffer;
+
+        /**
+         * Array where to store sample values computed by {@link #evaluateAtPixel(double,
double)}.
+         * For performance reasons, the same array may be recycled on every method call.
+         */
+        private double[] values;
+
+        /**
+         * The exponent separator used in the format, or {@code null} if none.
+         * This is a workaround for forcing a position sign in exponent
+         * (there is no {@link DecimalFormat} API for doing that).
+         */
+        @Workaround(library="JDK", version="14")
+        private final String exponentSeparator;
+
+        /**
+         * Creates a new evaluator for the given number of fraction digits.
+         *
+         * @param  data  the image from which sample values will be read.
+         */
+        Evaluator(final RenderedImage data) {
+            this.data = data;
+            buffer = new StringBuffer();
+            pos    = new FieldPosition(0);
+            format = NumberFormat.getNumberInstance();
+            ImageUtilities.getFractionDigits(data, 0).ifPresent((n) -> {
+                if (n < 0) n = 0;
+                format.setMinimumFractionDigits(n);
+                format.setMaximumFractionDigits(n);
+            });
+            /*
+             * Following menu items are hard-coded for now, may need to become configurable
in a future version.
+             */
+            valueChoices.setText(Resources.format(Resources.Keys.PositionalErrors));
+            if (format instanceof DecimalFormat) {
+                final DecimalFormat decimal = (DecimalFormat) format;
+                final String pattern = decimal.toPattern();
+                final CheckMenuItem item = new CheckMenuItem(Vocabulary.format(Vocabulary.Keys.ScientificNotation));
+                valueChoices.getItems().add(item);
+                item.selectedProperty().addListener((p,o,n) -> {
+                    decimal.applyPattern(n ? "0.000E00" : pattern);
+                });
+                exponentSeparator = decimal.getDecimalFormatSymbols().getExponentSeparator();
+            } else {
+                exponentSeparator = null;
+            }
+        }
+
+        /**
+         * Returns {@code false} since this evaluator is never empty.
+         */
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        /**
+         * Returns {@code null} for telling the caller to invoke {@link #evaluateAtPixel(double,
double)} instead.
+         */
+        @Override
+        public String evaluate(final DirectPosition point) {
+            return null;
+        }
+
+        /**
+         * Returns a string representation of data under given pixel coordinates in the canvas.
+         * Coordinates (0,0) are located in the upper-left canvas corner.
+         *
+         * @param  fx  0-based column index in the canvas. May be negative for coordinates
out of bounds.
+         * @param  fy  0-based row index in the canvas. May be negative for coordinates out
of bounds.
+         * @return string representation of data under given pixel.
+         */
+        @Override
+        public String evaluateAtPixel(final double fx, final double fy) {
+            try {
+                final int  x = Math.toIntExact(Math.round(fx));
+                final int  y = Math.toIntExact(Math.round(fy));
+                final int tx = ImageUtilities.pixelToTileX(data, x);
+                final int ty = ImageUtilities.pixelToTileY(data, y);
+                synchronized (buffer) {
+                    buffer.setLength(0);
+                    values = data.getTile(tx, ty).getPixel(x, y, values);
+                    format.format(values[0], buffer, pos);
+                    /*
+                     * Insert a positive sign after exponent separator if needed.
+                     * E.g. instead of "1.234E05" we want "1.234E+05".
+                     * The intent is to have number of fixed length.
+                     */
+                    if (exponentSeparator != null) {
+                        int i = buffer.lastIndexOf(exponentSeparator);
+                        if (i >= 0) {
+                            i += exponentSeparator.length();
+                            if (i < buffer.length() && Character.isDigit(buffer.codePointAt(i)))
{
+                                buffer.insert(i, '+');
+                            }
+                        }
+                    }
+                    // Unit of measurement ("px") hard coded for now.
+                    return buffer.append(" px").toString();
+                }
+            } catch (ArithmeticException | IllegalArgumentException | IndexOutOfBoundsException
e) {
+                // Position outside image. No value to show.
+                return null;
+            }
+        }
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
index 4246f04..f955271 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
@@ -1036,10 +1036,19 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent>
{
                 position.setMinWidth(Math.min(view.getWidth() / 2, Math.ceil(position.prefWidth(position.getHeight()))));
             }
             /*
-             * Format the values under cursor if the coordinates are valid.
+             * Format the values under cursor if the coordinates are valid. First try to
use coordinates
+             * in the CRS used by this status bar. If `ValuesUnderCursor` can not use those
"real world"
+             * coordinates, fallback on pixel coordinates.
              */
             if (isSampleValuesVisible) {
-                sampleValues.setText(success ? sampleValuesProvider.get().evaluate(targetCoordinates)
: null);
+                text = null;
+                if (success) {
+                    final ValuesUnderCursor sp = sampleValuesProvider.get();
+                    if ((text = sp.evaluate(targetCoordinates)) == null) {
+                        text = sp.evaluateAtPixel(x, y);
+                    }
+                }
+                sampleValues.setText(text);
             }
         }
     }
@@ -1070,6 +1079,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent>
{
             }
         }
         /*
+         * Mouse exited the canvas. Use substitution texts.
          * Do not use `position.setVisible(false)` because
          * we want the Tooltip to continue to be available.
          */
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
index ce26f94..c2403f5 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
@@ -60,7 +60,14 @@ import org.apache.sis.util.resources.Vocabulary;
 /**
  * Provider of textual content to show in a {@link StatusBar} for values under cursor position.
  * Different subtypes are defined for different data sources such as {@link GridCoverage}.
+ * When the mouse cursor moves, {@link #evaluate(DirectPosition)} is invoked with the same
+ * "real world" coordinates than the ones shown in the status bar.
+ * If that method can not provide sample values, then {@link #evaluateAtPixel(double, double)}
+ * is invoked as a fallback with pixel coordinates relative to the canvas.
+ *
+ * <h2>Multi-threading</h2>
  * Instances of {@code ValueUnderCursor} do not need to be thread-safe.
+ * {@code ValuesUnderCursor} methods will be invoked from JavaFX thread.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -92,7 +99,8 @@ public abstract class ValuesUnderCursor {
     private String lastErrorMessage;
 
     /**
-     * Creates a new instance.
+     * Creates a new evaluator instance. The {@link #valueChoices} list of items is initially
empty;
+     * subclass constructor should set a text and add items.
      */
     protected ValuesUnderCursor() {
         valueChoices = new Menu();
@@ -108,8 +116,10 @@ public abstract class ValuesUnderCursor {
     public abstract boolean isEmpty();
 
     /**
-     * Returns a string representation of data under given position.
-     * The position may be in any CRS; this method should convert coordinates as needed.
+     * Returns a string representation of data under given "real world" position.
+     * The {@linkplain DirectPosition#getCoordinateReferenceSystem() position CRS}
+     * should be non-null for avoiding ambiguity about what is the default CRS.
+     * The position CRS may be anything; this method shall transform coordinates itself if
needed.
      *
      * @param  point  the cursor location in arbitrary CRS (usually the CRS shown in the
status bar).
      *                May be {@code null} for declaring that the point is outside canvas
region.
@@ -118,6 +128,19 @@ public abstract class ValuesUnderCursor {
     public abstract String evaluate(final DirectPosition point);
 
     /**
+     * Returns a string representation of data under given pixel coordinates in the canvas.
+     * This method is invoked as a fallback if {@link #evaluate(DirectPosition)} returned
{@code null}.
+     * Coordinates (0,0) are located in the upper-left canvas corner.
+     *
+     * @param  x  0-based column index in the canvas. May be negative for coordinates out
of bounds.
+     * @param  y  0-based row index in the canvas. May be negative for coordinates out of
bounds.
+     * @return string representation of data under given pixel, or {@code null} if none.
+     */
+    public String evaluateAtPixel(double x, double y) {
+        return null;
+    }
+
+    /**
      * Invoked when a new source of values is known for computing the expected size.
      * The given {@code main} text should be an example of the longest expected text,
      * ignoring "special" labels like "no data" values (those special cases are listed
@@ -189,8 +212,8 @@ public abstract class ValuesUnderCursor {
 
     /**
      * Provider of textual content to show in {@link StatusBar} for {@link GridCoverage}
values under cursor position.
-     * This object should be registered as a listener of some coverage property,
-     * for example {@link CoverageCanvas#coverageProperty}.
+     * This object can be registered as a listener of e.g. {@link CoverageCanvas#coverageProperty}
for updating the
+     * values to show when the coverage is changed.
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @version 1.1
@@ -270,6 +293,7 @@ public abstract class ValuesUnderCursor {
             field         = new FieldPosition(0);
             nodata        = new HashMap<>();
             selectedBands = new BitSet();
+            valueChoices.setText(Vocabulary.format(Vocabulary.Keys.SampleDimensions));
         }
 
         /**
@@ -344,7 +368,6 @@ public abstract class ValuesUnderCursor {
             final Locale                    locale        = getLocale(property);
             final UnitFormat                unitFormat    = new UnitFormat(locale);
             final CheckMenuItem[]           menuItems     = new CheckMenuItem[n];
-            valueChoices.setText(Vocabulary.getResources(locale).getString(Vocabulary.Keys.SampleDimensions));
             for (int i=0; i<n; i++) {
                 final SampleDimension sd = bands.get(i);
                 menuItems[i] = createMenuItem(i, sd, locale);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PositionalErrorImage.java
b/core/sis-feature/src/main/java/org/apache/sis/image/PositionalErrorImage.java
index 77ee5a1..e993250 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PositionalErrorImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PositionalErrorImage.java
@@ -96,6 +96,39 @@ final class PositionalErrorImage extends ComputedImage {
     }
 
     /**
+     * Gets a property from this image. Current default implementation supports the following
keys
+     * (more properties may be added to this list in future Apache SIS versions):
+     *
+     * <ul>
+     *   <li>{@value #SAMPLE_RESOLUTIONS_KEY}</li>
+     * </ul>
+     */
+    @Override
+    public Object getProperty(final String key) {
+        if (SAMPLE_RESOLUTIONS_KEY.equals(key)) {
+            /*
+             * Division by 8 is an arbitrary value for having one more digit
+             * and keep a number having an exact representation in base 2.
+             */
+            return new float[] {(float) (ResamplingGrid.TOLERANCE / 8)};
+        } else {
+            return super.getProperty(key);
+        }
+    }
+
+    /**
+     * Returns the names of all recognized properties.
+     *
+     * @return names of all recognized properties.
+     */
+    @Override
+    public String[] getPropertyNames() {
+        return new String[] {
+            SAMPLE_RESOLUTIONS_KEY
+        };
+    }
+
+    /**
      * Returns the minimum <var>x</var> coordinate (inclusive) of this image.
      */
     @Override
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
index 9506d0c..d138634 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
@@ -273,7 +273,7 @@ public class ResampledImage extends ComputedImage {
     }
 
     /**
-     * Computes the {@link #POSITIONAL_ERRORS_KEY} value. This method is invoked by {@link
#getProperty(String)}
+     * Computes the {@value #POSITIONAL_ERRORS_KEY} value. This method is invoked by {@link
#getProperty(String)}
      * when the {@link #POSITIONAL_ERRORS_KEY} property value is requested. The result is
saved by weak reference
      * since recomputing this image is rarely requested, and if needed can be recomputed
easily.
      */
@@ -346,16 +346,19 @@ public class ResampledImage extends ComputedImage {
     }
 
     /**
-     * Gets a property from this image. Current default implementation forwards the following
property requests
-     * to the source image (more properties may be added to this list in future Apache SIS
versions):
+     * Gets a property from this image. Current default implementation supports the following
keys
+     * (more properties may be added to this list in future Apache SIS versions):
      *
      * <ul>
-     *   <li>{@value #SAMPLE_RESOLUTIONS_KEY}</li>
+     *   <li>{@value #POSITIONAL_ERRORS_KEY}</li>
+     *   <li>{@value #SAMPLE_RESOLUTIONS_KEY} (forwarded to the source image)</li>
      * </ul>
      *
-     * Above listed properties are selected because they should have approximately the same
values before and after
+     * <div class="note"><b>Note:</b>
+     * the sample resolutions are retained because they should have approximately the same
values before and after
      * resampling. {@linkplain #STATISTICS_KEY Statistics} are not in this list because,
while minimum and maximum
      * values should stay approximately the same, the average value and standard deviation
may be quite different.
+     * </div>
      */
     @Override
     public Object getProperty(final String key) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index b6cb98f..9f35edc 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -17,6 +17,8 @@
 package org.apache.sis.internal.coverage.j2d;
 
 import java.util.Arrays;
+import java.util.OptionalInt;
+import java.lang.reflect.Array;
 import java.awt.Rectangle;
 import java.awt.color.ColorSpace;
 import java.awt.geom.AffineTransform;
@@ -32,9 +34,12 @@ import java.awt.image.SinglePixelPackedSampleModel;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.util.Numbers;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.image.PlanarImage;
 
 import static java.lang.Math.abs;
 import static java.lang.Math.rint;
@@ -158,6 +163,28 @@ public final class ImageUtilities extends Static {
     }
 
     /**
+     * Returns the number of fraction digits to use for formatting sample values in the given
band of the given image.
+     * This method use the {@value PlanarImage#SAMPLE_RESOLUTIONS_KEY} property value.
+     *
+     * @param  image  the image from which to get the number of fraction digits.
+     * @param  band   the band for which to get the number of fraction digits.
+     * @return number of fraction digits. Maybe a negative number if the sample resolution
is equal or greater than 10.
+     */
+    public static OptionalInt getFractionDigits(final RenderedImage image, final int band)
{
+        final Object property = image.getProperty(PlanarImage.SAMPLE_RESOLUTIONS_KEY);
+        if (property != null) {
+            final int c = Numbers.getEnumConstant(property.getClass().getComponentType());
+            if (c >= Numbers.BYTE && c <= Numbers.BIG_DECIMAL && band
< Array.getLength(property)) {
+                final double resolution = Math.abs(((Number) Array.get(property, band)).doubleValue());
+                if (resolution > 0 && resolution <= Double.MAX_VALUE) {   
 // Non-zero, non-NaN and finite.
+                    return OptionalInt.of(DecimalFunctions.fractionDigitsForDelta(resolution,
false));
+                }
+            }
+        }
+        return OptionalInt.empty();
+    }
+
+    /**
      * Returns the data type of the given image.
      *
      * @param  image  the image for which to get the data type, or {@code null}.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index e7e080e..d4aacbe 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -975,6 +975,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Scale = 179;
 
         /**
+         * Scientific notation
+         */
+        public static final short ScientificNotation = 234;
+
+        /**
          * Simplified
          */
         public static final short Simplified = 180;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index f0224e2..5c6b5d3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -198,6 +198,7 @@ Root                    = Root
 RootMeanSquare          = Root Mean Square
 SampleDimensions        = Sample dimensions
 Scale                   = Scale
+ScientificNotation      = Scientific notation
 Simplified              = Simplified
 SlashSeparatedList_2    = {0}/{1}
 Source                  = Source
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 84dd01e..183e0b3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -205,6 +205,7 @@ Root                    = Racine
 RootMeanSquare          = Moyenne quadratique
 SampleDimensions        = Dimensions d\u2019\u00e9chantillonnage
 Scale                   = \u00c9chelle
+ScientificNotation      = Notation scientifique
 Simplified              = Simplifi\u00e9
 SlashSeparatedList_2    = {0}/{1}
 Source                  = Source


Mime
View raw message