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 b4cf213bf313bd3564fb2e6faab233cb6e315cf5
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Sep 17 12:15:09 2022 +0200

    Move `ValueUnderCursor.FromCoverage` as a top-level class for easier 
maintenance.
    Refactor for fetching and formatting values in a background thread instead 
of JavaFX thread.
---
 .../apache/sis/gui/coverage/GridSliceSelector.java |   1 +
 .../java/org/apache/sis/gui/map/StatusBar.java     |  68 +-
 .../org/apache/sis/gui/map/ValuesFormatter.java    | 522 +++++++++++++++
 .../org/apache/sis/gui/map/ValuesFromCoverage.java | 264 ++++++++
 .../org/apache/sis/gui/map/ValuesUnderCursor.java  | 699 +++++++--------------
 5 files changed, 1041 insertions(+), 513 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
index b7040e2c32..ba41db59f4 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
@@ -194,6 +194,7 @@ public class GridSliceSelector extends Widget {
     private void setGridGeometry(final GridGeometry gg) {
         final ObservableList<Node> children = view.getChildren();
         if (gg == null || gg.getDimension() <= BIDIMENSIONAL || 
!gg.isDefined(GridGeometry.EXTENT)) {
+            selectedExtent.set(null);
             children.clear();
             return;
         }
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 7e62bffa90..33049342e8 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
@@ -564,7 +564,7 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
             ValuesUnderCursor.update(this, o, n);
             if (o != null) items.remove(o.valueChoices);
             if (n != null) items.add(1, n.valueChoices);
-            setSampleValuesVisible(n != null && !n.isEmpty());
+            setSampleValuesVisible(n != null);
         });
     }
 
@@ -1061,7 +1061,7 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
         }
         target.setCoordinateReferenceSystem(crs);
         format.setDefaultCRS(crs);
-        targetCoordinates = target;         // Assign only after abpve succeed.
+        targetCoordinates = target;         // Assign only after above succeed.
         formatAsIdentifiers = null;
         format.setGroundAccuracy(Quantities.max(accuracy, 
lowestAccuracy.get()));
         setTooltip(crs);
@@ -1365,17 +1365,11 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      */
     public void setLocalCoordinates(final double x, final double y) {
         if (x != lastX || y != lastY) {
-            String text = formatLocalCoordinates(lastX = x, lastY = y);
-            position.setText(text);
             if (isSampleValuesVisible) {
-                String values;
-                try {
-                    values = 
sampleValuesProvider.get().evaluate(targetCoordinates);
-                } catch (RuntimeException e) {
-                    values = cause(e);
-                }
-                sampleValues.setText(values);
+                sampleValuesProvider.get().evaluateLater(targetCoordinates);   
     // Work in a background thread.
             }
+            final String text = formatLocalCoordinates(lastX = x, lastY = y);
+            position.setText(text);
             /*
              * Make sure that there is enough space for keeping the 
coordinates always visible.
              * This is needed if there is an error message on the left which 
may be long.
@@ -1515,10 +1509,17 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
         lastX = lastY = Double.NaN;
         position.setText(outsideText);
         if (isSampleValuesVisible) {
-            sampleValues.setText(sampleValuesProvider.get().evaluate(null));
+            sampleValuesProvider.get().evaluateLater(null);
         }
     }
 
+    /**
+     * Sets the result of formatting sample values under cursor position.
+     */
+    final void setSampleValues(final String text) {
+        sampleValues.setText(text);
+    }
+
     /**
      * Returns {@code true} if the position contains a valid coordinates.
      */
@@ -1539,21 +1540,23 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      */
     private void setSampleValuesVisible(final boolean visible) {
         final ObservableList<Node> c = view.getChildren();
+        Label view = sampleValues;
         if (visible) {
-            if (sampleValues == null) {
-                sampleValues = new Label();
-                sampleValues.setAlignment(Pos.CENTER_RIGHT);
-                sampleValues.setTextAlignment(TextAlignment.RIGHT);
-                sampleValues.setMinWidth(Label.USE_PREF_SIZE);
-                sampleValues.setMaxWidth(Label.USE_PREF_SIZE);
+            if (view == null) {
+                view = new Label();
+                view.setAlignment(Pos.CENTER_RIGHT);
+                view.setTextAlignment(TextAlignment.RIGHT);
+                view.setMinWidth(Label.USE_PREF_SIZE);
+                view.setMaxWidth(Label.USE_PREF_SIZE);
+                sampleValues = view;
             }
-            if (c.lastIndexOf(sampleValues) < 0) {
+            if (c.lastIndexOf(view) < 0) {
                 final Separator separator = new 
Separator(Orientation.VERTICAL);
-                c.addAll(separator, sampleValues);
+                c.addAll(separator, view);
             }
-        } else if (sampleValues != null) {
-            sampleValues.setText(null);
-            int i = c.lastIndexOf(sampleValues);
+        } else if (view != null) {
+            view.setText(null);
+            int i = c.lastIndexOf(view);
             if (i >= 0) {
                 c.remove(i);
                 if (--i >= 0) {
@@ -1570,7 +1573,7 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      * If {@code prototype} is empty, then no sample values are expected and 
the {@link #sampleValues} label will be
      * hidden.
      *
-     * @param  prototype  an example of longest normal text that we expect.
+     * @param  prototype  an example of longest normal text that we expect, or 
{@code null} or empty for hiding.
      * @param  others     some other texts that may appear, such as labels for 
missing data.
      * @return {@code true} on success, or {@code false} if this method should 
be invoked again.
      *
@@ -1579,13 +1582,14 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
     final boolean computeSizeOfSampleValues(final String prototype, final 
Iterable<String> others) {
         setSampleValuesVisible(prototype != null && !prototype.isEmpty());
         if (isSampleValuesVisible) {
-            sampleValues.setText(prototype);
-            sampleValues.setPrefWidth(Label.USE_COMPUTED_SIZE);                
 // Enable `prefWidth(…)` computation.
-            double width = sampleValues.prefWidth(sampleValues.getHeight());
+            final Label view = sampleValues;
+            view.setText(prototype);
+            view.setPrefWidth(Label.USE_COMPUTED_SIZE);                 // 
Enable `prefWidth(…)` computation.
+            double width = view.prefWidth(view.getHeight());
             final double max = Math.max(width * 1.25, 200);                    
 // Arbitrary limit.
             for (final String other : others) {
-                sampleValues.setText(other);
-                final double cw = 
sampleValues.prefWidth(sampleValues.getHeight());
+                view.setText(other);
+                final double cw = view.prefWidth(view.getHeight());
                 if (cw > width) {
                     width = cw;
                     if (width > max) {
@@ -1594,11 +1598,11 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
                     }
                 }
             }
-            sampleValues.setText(null);
+            view.setText(null);
             if (!(width > 0)) {                 // May be 0 if canvas is not 
yet added to scene graph.
                 return false;
             }
-            sampleValues.setPrefWidth(width + VALUES_PADDING);
+            view.setPrefWidth(width + VALUES_PADDING);
         }
         return true;
     }
@@ -1722,7 +1726,7 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      * @param  e  the exception.
      * @return the exception message or class name.
      */
-    private String cause(Throwable e) {
+    final String cause(Throwable e) {
         if (e instanceof Exception) {
             e = Exceptions.unwrap((Exception) e);
         }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFormatter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFormatter.java
new file mode 100644
index 0000000000..8df7202973
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFormatter.java
@@ -0,0 +1,522 @@
+/*
+ * 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.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.BitSet;
+import java.util.Locale;
+import java.util.Optional;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.DecimalFormat;
+import javax.measure.Unit;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.metadata.content.TransferFunctionType;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometry.AbstractDirectPosition;
+import org.apache.sis.referencing.operation.transform.TransferFunction;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.measure.UnitFormat;
+import org.apache.sis.util.Characters;
+import org.apache.sis.util.logging.Logging;
+
+import static java.util.logging.Logger.getLogger;
+
+// Branch-dependent imports
+import org.opengis.coverage.CannotEvaluateException;
+
+
+/**
+ * Fetches values from the coverage and formats them. This task is executed in 
a background thread
+ * because calls to {@link GridCoverage#render(GridExtent)} can take an 
arbitrary amount of time.
+ * The same {@code Formatter} instance can be reused as long as the 
configuration does not change.
+ *
+ * <p>As a rule of thumbs, all fields in {@link ValuesFromCoverage} class 
shall be read and written
+ * from the JavaFX thread, while all fields in this {@code Formatter} class 
can be read and written
+ * from a background thread.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.1
+ * @module
+ */
+final class ValuesFormatter extends ValuesUnderCursor.Formatter {
+    /**
+     * The separator to insert between sample values. We use EM space.
+     */
+    private static final char SEPARATOR = '\u2003';
+
+    /**
+     * Pseudo amount of fraction digits for default format.
+     * Used when we don't know how many fraction digits to use.
+     */
+    private static final int DEFAULT_FORMAT = -1;
+
+    /**
+     * Pseudo amount of fraction digits for scientific notation.
+     */
+    private static final int SCIENTIFIC_NOTATION = -2;
+
+    /**
+     * The object computing or interpolation sample values in the coverage.
+     */
+    private final GridCoverage.Evaluator evaluator;
+
+    /**
+     * Formatter for the values computed or interpolated by {@link #evaluator}.
+     * The number of fraction digits is computed from transfer function 
resolution.
+     * The same {@link NumberFormat} instance may appear at more than one 
index.
+     * Array shall not be modified after construction.
+     */
+    private final NumberFormat[] sampleFormats;
+
+    /**
+     * Buffer where to format the textual content.
+     * We use this buffer as a synchronization lock because this class is 
already synchronized,
+     * so synchronizing on {@cod buffer} allows us to use only one lock.
+     */
+    private final StringBuffer buffer;
+
+    /**
+     * Ignored but required by {@link NumberFormat}.
+     */
+    private final FieldPosition field;
+
+    /**
+     * Unit symbol to write after each value.
+     * Array shall not be modified after construction.
+     */
+    private final String[] units;
+
+    /**
+     * The text to show when value under cursor is a NaN value.
+     * Values are packed with band number in low bits and float ordinal value 
in high bits.
+     * Map content shall not be modified after construction.
+     *
+     * @see #toNodataKey(int, float)
+     * @see MathFunctions#toNanOrdinal(float)
+     */
+    private final Map<Long,String> nodata;
+
+    /**
+     * The text to show when cursor is outside coverage area. It should 
contain dimension names, for example "(SST)".
+     * May be {@code null} if {@link #setSelectedBands(BitSet, String[], 
HashSet)} needs to be invoked.
+     */
+    private String outsideText;
+
+    /**
+     * The selection status of each band at the time of {@link 
#setSelectedBands(BitSet, String[], HashSet)} invocation.
+     * We need a copy of {@link ValuesFromCoverage} field because the two sets 
are read and updated in different threads.
+     * This set should not be modified; instead, copy should be made.
+     *
+     * @see ValuesFromCoverage#selectedBands
+     */
+    private BitSet selectedBands;
+
+    /**
+     * Non-null when a new slice needs to be passed to {@link #evaluator}.
+     * A new value is set when {@link CoverageCanvas#sliceExtentProperty} 
changed.
+     * This value is reset to {@code null} after the slice has been taken in 
account.
+     *
+     * @see #setSlice(GridExtent)
+     */
+    private GridExtent newSlice;
+
+    /**
+     * Creates a new formatter for the specified coverage.
+     * This constructor should be invoked in a background thread.
+     *
+     * @param owner     the instance which will evaluate values under cursor 
position.
+     * @param inherit   formatter from which to inherit band configuration, or 
{@code null} if none.
+     * @param coverage  new coverage. Shall not be null.
+     * @param slice     initial value of {@link #newSlice}.
+     * @param bands     sample dimensions of the new coverage.
+     * @param locale    locale of number formats to create.
+     */
+    ValuesFormatter(final ValuesUnderCursor owner, final ValuesFormatter 
inherit, final GridCoverage coverage,
+                    final GridExtent slice, final List<SampleDimension> bands, 
final Locale locale)
+    {
+        super(owner);
+        buffer    = new StringBuffer();
+        field     = new FieldPosition(0);
+        newSlice  = slice;
+        evaluator = coverage.forConvertedValues(true).evaluator();
+        evaluator.setNullIfOutside(true);
+        evaluator.setWraparoundEnabled(true);
+        selectedBands = new BitSet();
+        if (inherit != null) {
+            // Same configuration than previous coverage.
+            synchronized (inherit.buffer) {
+                units         = inherit.units;
+                nodata        = inherit.nodata;
+                outsideText   = inherit.outsideText;
+                sampleFormats = inherit.sampleFormats.clone();
+                for (int i=0; i < sampleFormats.length; i++) {
+                    sampleFormats[i] = (NumberFormat) sampleFormats[i].clone();
+                }
+            }
+            return;
+        }
+        final int numBands = bands.size();
+        sampleFormats = new NumberFormat[numBands];
+        units         = new String[numBands];
+        nodata        = new HashMap<>();
+        /*
+         * Loop below initializes number formats and unit symbols for all 
bands, regardless
+         * if selected or not. We do that on the assumption that the same 
format and symbol
+         * are typically shared by all bands.
+         */
+        final Map<Integer,NumberFormat> sharedFormats = new HashMap<>();
+        final Map<Unit<?>,String>       sharedSymbols = new HashMap<>();
+        final UnitFormat                unitFormat    = new UnitFormat(locale);
+        for (int b=0; b<numBands; b++) {
+            /*
+             * Build the list of texts to show for missing values. A coverage 
can have
+             * different NaN values representing different kind of missing 
values.
+             */
+            final SampleDimension sd = bands.get(b);
+            for (final Category c : 
sd.forConvertedValues(true).getCategories()) {
+                final float value = ((Number) 
c.getSampleRange().getMinValue()).floatValue();
+                if (Float.isNaN(value)) try {
+                    nodata.putIfAbsent(toNodataKey(b, value), 
c.getName().toString(locale));
+                } catch (IllegalArgumentException e) {
+                    recoverableException("changed", e);
+                }
+            }
+            /*
+             * Format in advance the units of measurement. If none, an empty 
string is used.
+             * Note: it is quite common that all bands use the same unit of 
measurement.
+             */
+            units[b] = sd.getUnits().map((unit) -> 
sharedSymbols.computeIfAbsent(unit,
+                                          (key) -> format(unitFormat, 
key))).orElse("");
+            /*
+             * Infer a number of fraction digits to use for the resolution of 
sample values in each band.
+             */
+            final SampleDimension isd = sd.forConvertedValues(false);
+            final Integer nf = isd.getTransferFunctionFormula().map(
+                    (formula) -> suggestFractionDigits(formula, 
isd)).orElse(DEFAULT_FORMAT);
+            /*
+             * Create number formats with a number of fraction digits inferred 
from sample value resolution.
+             * The same format instances are shared when possible. Keys are 
the number of fraction digits.
+             * Special values:
+             *
+             *   - Key  0 is for integer values.
+             *   - Key -1 is for default format with unspecified number of 
fraction digits.
+             *   - Key -2 is for scientific notation.
+             */
+            sampleFormats[b] = sharedFormats.computeIfAbsent(nf, (precision) 
-> {
+                switch (precision) {
+                    case 0:              return 
NumberFormat.getIntegerInstance(locale);
+                    case DEFAULT_FORMAT: return 
NumberFormat.getNumberInstance(locale);
+                    case SCIENTIFIC_NOTATION: {
+                        final NumberFormat format = 
NumberFormat.getNumberInstance(locale);
+                        if (precision == SCIENTIFIC_NOTATION && format 
instanceof DecimalFormat) {
+                            ((DecimalFormat) format).applyPattern("0.000E00");
+                        }
+                        return format;
+                    }
+                    default: {
+                        final NumberFormat format = 
NumberFormat.getNumberInstance(locale);
+                        format.setMinimumFractionDigits(precision);
+                        format.setMaximumFractionDigits(precision);
+                        return format;
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Formats the unit symbol to append after a sample value. The unit 
symbols are created in advance
+     * and reused for all sample value formatting as long as the sample 
dimensions do not change.
+     */
+    private String format(final UnitFormat format, final Unit<?> unit) {
+        synchronized (buffer) {         // Take lock once instead of at each 
StringBuffer method call.
+            buffer.setLength(0);
+            format.format(unit, buffer, field);
+            if (buffer.length() != 0 && 
Character.isLetterOrDigit(buffer.codePointAt(0))) {
+                buffer.insert(0, Characters.NO_BREAK_SPACE);
+            }
+            return buffer.toString();
+        }
+    }
+
+    /**
+     * Formats the widest text that we expect. This text is used for computing 
the label width.
+     * Also computes the text to show when cursor is outside coverage area. 
This method is invoked
+     * when the bands selection changed, either because of selection in 
contextual menu or because
+     * {@link ValuesUnderCursor} is providing data for a new coverage.
+     *
+     * <p>We use {@link ValuesFromCoverage#needsBandRefresh} as a flag meaning 
that this method needs to be invoked.
+     * This method invocation sometime needs to be delayed because calculation 
of text width may be wrong
+     * (produce 0 values) if invoked before {@link StatusBar#sampleValues} 
label is added in the scene graph.</p>
+     *
+     * <p>This method uses the same synchronization lock than {@link 
#evaluate(DirectPosition)}.
+     * Consequently this method may block if data loading are in progress in 
another thread.</p>
+     *
+     * @param  selection  copy of {@link ValuesFromCoverage#selectedBands} 
made by the caller in JavaFX thread.
+     * @param  labels     labels of {@link ValuesFromCoverage#valueChoices} 
menu items computed by caller in JavaFX thread.
+     * @param  others     an initially empty set where to put textual 
representation of "no data" values.
+     * @return the text to use as a prototype for sample values.
+     */
+    final String setSelectedBands(final BitSet selection, final String[] 
labels, final HashSet<String> others) {
+        synchronized (buffer) {
+            final List<SampleDimension> bands = 
evaluator.getCoverage().getSampleDimensions();
+            final StringBuilder names = new StringBuilder().append('(');
+            buffer.setLength(0);
+            for (int i = -1; (i = selection.nextSetBit(i+1)) >= 0;) {
+                if (buffer.length() != 0) {
+                    buffer.append(SEPARATOR);
+                    names.append(", ");
+                }
+                names.append(labels[i]);
+                final int start = buffer.length();
+                final Comparable<?>[] sampleValues = 
bands.get(i).forConvertedValues(true)
+                        .getSampleRange().map((r) -> new Comparable<?>[] 
{r.getMinValue(), r.getMaxValue()})
+                        .orElseGet(() -> new Comparable<?>[] {0xFFFF});        
         // Arbitrary value.
+                for (final Comparable<?> value : sampleValues) {
+                    final int end = buffer.length();
+                    sampleFormats[i].format(value, buffer, field);
+                    final int length = buffer.length();
+                    if (length - end >= end - start) {
+                        buffer.delete(start, end);      // Delete first number 
if it was shorter.
+                    } else {
+                        buffer.setLength(end);          // Delete second 
number if it is shorter.
+                    }
+                }
+                buffer.append(units[i]);
+            }
+            final String text = buffer.toString();
+            /*
+             * At this point, `text` is the longest string of numerical values 
that we expect.
+             * We also need to take in account the width required for 
displaying "no data" labels.
+             * If a "no data" label is shown, it will be shown alone (we do 
not need to compute a
+             * sum of "no data" label widths).
+             */
+            for (final Map.Entry<Long,String> other : nodata.entrySet()) {
+                if (selection.get(other.getKey().intValue())) {
+                    others.add(other.getValue());
+                }
+            }
+            outsideText = text.isEmpty() ? "" : names.append(')').toString();
+            selectedBands = selection;              // Set only on success.
+            return text;
+        }
+    }
+
+    /**
+     * Sets the slice in grid coverages where sample values should be 
evaluated for next positions.
+     * The given slice will apply to all positions formatted after this method 
call,
+     * until this method is invoked again for a new slice.
+     *
+     * <p>This method shall be synchronized on the same lock than {@link 
#copy(DirectPosition)},
+     * which is the lock used by {@link #evaluateLater(DirectPosition)}.</p>
+     *
+     * @param  slice  grid coverage slice where to evaluate the sample values.
+     */
+    final synchronized void setSlice(final GridExtent slice) {
+        newSlice = slice;
+    }
+
+    /**
+     * Position of next point to evaluate, together with the grid slice where 
sample values should be evaluated.
+     * Those two information are kept together because they are closely 
related: the slice depends on position
+     * in dimensions not necessarily expressed in the given {@link 
DirectPosition}, and we want to take those
+     * two information in the same synchronized block.
+     */
+    private static final class Position extends AbstractDirectPosition {
+        /** Coordinates of this position. */
+        private final double[] coordinates;
+
+        /** Coordinate reference system of this position. */
+        private final CoordinateReferenceSystem crs;
+
+        /**
+         * Non-null when a new slice needs to be passed to {@link #evaluator}.
+         * Should be null if the slice did not changed since last invocation.
+         * This is a copy of {@link ValuesFormatter#newSlice}.
+         */
+        final GridExtent newSlice;
+
+        /**
+         * Creates a copy of the given position. If {@link #evaluator} needs 
to be set
+         * to a new default slice position in order to evaluate the given 
coordinates,
+         * that position should be given as a non-null {@code slice} argument.
+         */
+        Position(final DirectPosition position, final GridExtent slice) {
+            coordinates = position.getCoordinate();
+            crs         = position.getCoordinateReferenceSystem();
+            newSlice    = slice;
+        }
+
+        /** Returns the number of dimensions of this position. */
+        @Override public int getDimension() {
+            return coordinates.length;
+        }
+
+        /** Returns the coordinate value in given dimension. */
+        @Override public double getOrdinate(final int dimension) {
+            return coordinates[dimension];
+        }
+
+        /** Returns the CRS of this position, or {@code null} if unspecified. 
*/
+        @Override public CoordinateReferenceSystem 
getCoordinateReferenceSystem() {
+            return crs;
+        }
+    }
+
+    /**
+     * Invoked in JavaFX thread for creating a copy of the given position 
together with related information.
+     * The related information is the grid coverage slice where the given 
position can be evaluated.
+     * The instance returned by this method will be given to {@link 
#evaluate(DirectPosition)}.
+     *
+     * @param  point  coordinates of the point for which to evaluate the grid 
coverage value.
+     * @return a copy of the given point, augmented with the slice where the 
point can be evaluated.
+     */
+    @Override
+    DirectPosition copy(final DirectPosition point) {
+        assert Thread.holdsLock(this);
+        final Position position = new Position(point, newSlice);
+        newSlice = null;
+        return position;
+    }
+
+    /**
+     * Computes a string representation of data under the given position.
+     * The position may be in any CRS; this method will convert coordinates as 
needed.
+     * This method should be invoked in a background thread.
+     *
+     * @param  point  the cursor location in arbitrary CRS, or {@code null} if 
outside canvas region.
+     * @return string representation of data under given position.
+     *
+     * @see GridCoverage.Evaluator#apply(DirectPosition)
+     */
+    @Override
+    public String evaluate(final DirectPosition point) {
+        synchronized (buffer) {
+            buffer.setLength(0);
+            if (point != null) try {
+                final GridExtent slice = ((Position) point).newSlice;       // 
This cast should never fail.
+                if (slice != null) {
+                    evaluator.setDefaultSlice(slice.getSliceCoordinates());
+                }
+                final double[] results = evaluator.apply(point);
+                if (results != null) {
+                    final BitSet selection = selectedBands;
+                    for (int i = -1; (i = selection.nextSetBit(i+1)) >= 0;) {
+                        if (buffer.length() != 0) {
+                            buffer.append(SEPARATOR);
+                        }
+                        final double value = results[i];
+                        if (Double.isNaN(value)) try {
+                            /*
+                             * If a value is NaN, returns its label as the 
whole content. Numerical values
+                             * in other bands are lost. We do that because "no 
data" strings are often too
+                             * long for being shown together with numerical 
values, and are often the same
+                             * for all bands. Users can see numerical values 
by hiding the band containing
+                             * "no data" values with contextual menu on the 
status bar.
+                             */
+                            final String label = nodata.get(toNodataKey(i, 
(float) value));
+                            if (label != null) {
+                                return label;
+                            }
+                        } catch (IllegalArgumentException e) {
+                            recoverableException("evaluate", e);
+                        }
+                        sampleFormats[i].format(value, buffer, 
field).append(units[i]);
+                    }
+                    return buffer.toString();
+                }
+            } catch (CannotEvaluateException e) {
+                recoverableException("evaluate", e);
+            }
+            /*
+             * Point is considered outside coverage area.
+             * We will write the sample dimension names.
+             */
+            return outsideText;
+        }
+    }
+
+    /**
+     * Returns the key to use in {@link #nodata} map for the given "no data" 
value.
+     * The band number can be obtained by {@link Long#intValue()}.
+     *
+     * @param  band   band index.
+     * @param  value  the NaN value used for "no data".
+     * @return key to use in {@link #nodata} map.
+     * @throws IllegalArgumentException if the given value is not a NaN value
+     *         or does not use a supported bits pattern.
+     */
+    private static Long toNodataKey(final int band, final float value) {
+        return (((long) MathFunctions.toNanOrdinal(value)) << Integer.SIZE) | 
band;
+    }
+
+    /**
+     * Suggests a number of fraction digits for numbers formatted after 
conversion by the given formula.
+     * This is either a positive number (including 0 for integers), or the 
{@value #SCIENTIFIC_NOTATION}
+     * or {@value #DEFAULT_FORMAT} sentinel values.
+     */
+    private static Integer suggestFractionDigits(final TransferFunction 
formula, final SampleDimension isd) {
+        int nf;
+        if (formula.getType() != TransferFunctionType.LINEAR) {
+            nf = SCIENTIFIC_NOTATION;
+        } else {
+            double resolution = formula.getScale();
+            if (resolution > 0 && resolution <= Double.MAX_VALUE) {     // 
Non-zero, non-NaN and finite.
+                final Optional<NumberRange<?>> range = isd.getSampleRange();
+                if (range.isPresent()) {
+                    // See StatusBar.inflatePrecisions for rationale.
+                    resolution *= (0.5 / range.get().getSpan()) + 1;
+                }
+                nf = DecimalFunctions.fractionDigitsForDelta(resolution, 
false);
+                if (nf < -9 || nf > 6) nf = SCIENTIFIC_NOTATION;        // 
Arbitrary thresholds.
+            } else {
+                nf = DEFAULT_FORMAT;
+            }
+        }
+        return nf;
+    }
+
+    /**
+     * Message of the last exception, used for avoiding flooding the logger 
with repetitive errors.
+     *
+     * @see #recoverableException(String, Exception)
+     */
+    private String lastErrorMessage;
+
+    /**
+     * Invoked when an exception occurred while computing values.
+     */
+    private void recoverableException(final String method, final Exception e) {
+        final String message = e.getMessage();
+        if (!message.equals(lastErrorMessage)) {
+            lastErrorMessage = message;
+            Logging.recoverableException(getLogger(Modules.APPLICATION), 
ValuesUnderCursor.class, method, e);
+        }
+    }
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFromCoverage.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFromCoverage.java
new file mode 100644
index 0000000000..03712fbf8b
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFromCoverage.java
@@ -0,0 +1,264 @@
+/*
+ * 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.List;
+import java.util.HashSet;
+import java.util.BitSet;
+import java.util.Locale;
+import java.text.NumberFormat;
+import javafx.concurrent.Task;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.property.ReadOnlyProperty;
+import javafx.collections.ObservableList;
+import javafx.scene.control.CheckMenuItem;
+import javafx.scene.control.MenuItem;
+import org.apache.sis.gui.coverage.CoverageCanvas;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.internal.gui.GUIUtilities;
+import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * Provider of textual content to show in {@link StatusBar} for {@link 
GridCoverage} values under cursor position.
+ * 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.
+ *
+ * <h2>Multi-threading</h2>
+ * This class fetches values and formats them in a background thread because 
calls
+ * to {@link GridCoverage#render(GridExtent)} can take an arbitrary amount of 
time.
+ * The {@link ValuesFormatter#buffer} is used as a synchronization lock.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.1
+ * @module
+ */
+final class ValuesFromCoverage extends ValuesUnderCursor implements 
ChangeListener<GridCoverage> {
+    /**
+     * The task to execute in a background thread for fetching values from the 
coverage and formatting them.
+     * This is {@code null} if there is no coverage. A new instance shall be 
created when the coverage changed.
+     */
+    private ValuesFormatter formatter;
+
+    /**
+     * The selection status of each band.
+     */
+    private final BitSet selectedBands;
+
+    /**
+     * {@code true} if {@link ValuesFormatter#setSelectedBands(BitSet, 
String[], HashSet)}
+     * needs to be invoked again.
+     */
+    private boolean needsBandRefresh;
+
+    /**
+     * {@code true} if {@link ValuesFormatter#setSelectedBands(BitSet, 
String[], HashSet)}
+     * is under execution in a background thread.
+     */
+    private boolean refreshing;
+
+    /**
+     * Creates a new provider of textual values for a {@link GridCoverage}.
+     */
+    public ValuesFromCoverage() {
+        selectedBands = new BitSet();
+        
valueChoices.setText(Vocabulary.format(Vocabulary.Keys.SampleDimensions));
+    }
+
+    /**
+     * Resets this {@code ValuesFromCoverage} to its initial state.
+     * This is invoked when there is no coverage to show, or in case of 
failure.
+     */
+    private void clear() {
+        formatter = null;
+        refreshing = false;
+        needsBandRefresh = false;
+        selectedBands.clear();
+        valueChoices.getItems().clear();
+    }
+
+    /**
+     * Sets the slice in grid coverages where sample values should be 
evaluated for next positions.
+     * This method is invoked when {@link CoverageCanvas#sliceExtentProperty} 
changed its value.
+     */
+    final void setSlice(final GridExtent extent) {
+        if (formatter != null) {
+            formatter.setSlice(extent);
+        }
+    }
+
+    /**
+     * Returns the slice extent specified in the canvas which contains the 
given property.
+     * This is a workaround for the fact that the selected slice is not an 
information
+     * provided directly by the {@link GridCoùverage} values.
+     */
+    private static GridExtent getSelectedSlice(final ObservableValue<?> 
property) {
+        if (property instanceof ReadOnlyProperty<?>) {
+            final Object bean = ((ReadOnlyProperty<?>) property).getBean();
+            if (bean instanceof CoverageCanvas) {
+                return ((CoverageCanvas) bean).getSliceExtent();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Notifies this {@code ValuesUnderCursor} object that it needs to display 
values for a new coverage.
+     * The {@code previous} argument should be the argument given in the last 
call to this method and is
+     * used as an optimization hint. In case of doubt, it can be {@code null}.
+     *
+     * @param  property  the property which has been updated, or {@code null} 
if unknown.
+     * @param  previous  previous property value, of {@code null} if none or 
unknown.
+     * @param  coverage  new coverage for which to show sample values, or 
{@code null} if none.
+     */
+    @Override
+    public void changed(final ObservableValue<? extends GridCoverage> property,
+                        final GridCoverage previous, final GridCoverage 
coverage)
+    {
+        if (coverage == null) {
+            clear();
+            return;
+        }
+        final GridExtent slice = getSelectedSlice(property);        // Need to 
be invoked in JavaFX thread.
+        final Locale locale = GUIUtilities.getLocale(property);
+        BackgroundThreads.execute(new Task<ValuesFormatter>() {
+            /**
+             * The formatter from which to inherit configuration if the sample 
dimensions did not changed.
+             * The initial {@link #formatter} value needs to be assigned from 
JavaFX thread.
+             */
+            private ValuesFormatter inherit = formatter;
+
+            /**
+             * Sample dimensions of the coverage, fetched in background thread 
in case it is costly to compute.
+             */
+            private List<SampleDimension> bands;
+
+            /**
+             * Invoked in a background thread for reconfiguring {@link 
ValuesFromCoverage}.
+             * This method creates a new formatter with new {@link 
NumberFormat} instances.
+             * If successful, the JavaFX components are updated by {@link 
#succeeded()}.
+             */
+            @Override protected ValuesFormatter call() {
+                bands = coverage.getSampleDimensions();
+                if (!(previous != null && 
bands.equals(previous.getSampleDimensions()))) {
+                    inherit = null;
+                }
+                return new ValuesFormatter(ValuesFromCoverage.this, inherit, 
coverage, slice, bands, locale);
+            }
+
+            /**
+             * Invoked in JavaFX thread after successful configuration by 
background thread.
+             * The formatter created in background thread is assigned to 
{@link #formatter},
+             * then the new menu items are created (unless they did not 
changed).
+             */
+            @Override protected void succeeded() {
+                formatter = getValue();
+                if (inherit == null) {
+                    /*
+                     * Only the first band is initially selected, unless the 
image has only 2 or 3 bands
+                     * in which case all bands are selected. An image with two 
bands is often giving the
+                     * (u,v) components of velocity vectors, which we want to 
keep together by default.
+                     */
+                    final int numBands = bands.size();
+                    final CheckMenuItem[] menuItems = new 
CheckMenuItem[numBands];
+                    final BitSet selection = selectedBands;
+                    selection.clear();
+                    selection.set(0, (numBands <= 3) ? numBands : 1, true);
+                    for (int b=0; b<numBands; b++) {
+                        menuItems[b] = createMenuItem(b, bands.get(b), locale);
+                    }
+                    valueChoices.getItems().setAll(menuItems);
+                    needsBandRefresh = true;
+                }
+            }
+
+            /**
+             * Invoked in JavaFX thread if an error occurred while 
initializing the formatter.
+             */
+            @Override protected void failed() {
+                clear();
+                setError(getException());
+            }
+        });
+    }
+
+    /**
+     * Creates a new menu item for the given sample dimension.
+     * This method shall be invoked from JavaFX thread.
+     *
+     * @param  index   index of the sample dimension.
+     * @param  sd      the sample dimension for which to create a menu item.
+     * @param  locale  the locale to use for fetching the sample dimension 
name.
+     */
+    private CheckMenuItem createMenuItem(final int index, final 
SampleDimension sd, final Locale locale) {
+        final CheckMenuItem item = new 
CheckMenuItem(sd.getName().toInternationalString().toString(locale));
+        item.setSelected(selectedBands.get(index));
+        item.selectedProperty().addListener((p,o,n) -> {
+            selectedBands.set(index, n);
+            needsBandRefresh = true;
+        });
+        return item;
+    }
+
+    /**
+     * Returns the task for fetching and formatting values in a background 
thread, or {@code null} if none.
+     * The formatter is created in a background thread as soon as the {@link 
MapCanvas} data are known.
+     * This method is invoked in JavaFX thread and may return {@code null} if 
the formatter is still
+     * under construction.
+     */
+    @Override
+    protected Formatter formatter() {
+        if (refreshing) {
+            return null;
+        }
+        final ValuesFormatter formatter = this.formatter;
+        if (formatter != null && needsBandRefresh && usePrototype()) {
+            final ObservableList<MenuItem> menus = valueChoices.getItems();
+            final String[] labels = new String[menus.size()];
+            for (int i=0; i<labels.length; i++) {
+                labels[i] = menus.get(i).getText();
+            }
+            final HashSet<String> others = new HashSet<>();
+            final BitSet selection = (BitSet) selectedBands.clone();
+            BackgroundThreads.execute(new Task<String>() {
+                /** Invoked in background thread for configuring the 
formatter. */
+                @Override protected String call() {
+                    return formatter.setSelectedBands(selection, labels, 
others);
+                }
+
+                /** Invoked in JavaFX thread if the configuration succeeded. */
+                @Override protected void succeeded() {
+                    needsBandRefresh = !prototype(getValue(), others);
+                    refreshing = false;
+                }
+
+                /** Invoked in JavaFX thread if the configuration failed. */
+                @Override protected void failed() {
+                    clear();
+                    setError(getException());
+                }
+            });
+            refreshing = true;
+        }
+        return formatter;
+    }
+}
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 8ec168b956..f256595343 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
@@ -16,55 +16,27 @@
  */
 package org.apache.sis.gui.map;
 
-import java.util.List;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.BitSet;
-import java.util.Locale;
-import java.util.Optional;
-import java.text.FieldPosition;
-import java.text.NumberFormat;
-import java.text.DecimalFormat;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.beans.property.ReadOnlyProperty;
+import java.util.concurrent.atomic.AtomicReference;
 import javafx.beans.value.WeakChangeListener;
-import javafx.collections.ObservableList;
 import javafx.scene.control.Menu;
-import javafx.scene.control.CheckMenuItem;
-import javafx.scene.control.MenuItem;
-import javax.measure.Unit;
+import javafx.application.Platform;
 import org.opengis.geometry.DirectPosition;
-import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.metadata.content.TransferFunctionType;
-import org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.gui.coverage.CoverageCanvas;
-import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.coverage.Category;
-import org.apache.sis.internal.system.Modules;
-import org.apache.sis.internal.gui.GUIUtilities;
-import org.apache.sis.math.DecimalFunctions;
-import org.apache.sis.math.MathFunctions;
-import org.apache.sis.measure.NumberRange;
-import org.apache.sis.measure.UnitFormat;
-import org.apache.sis.util.Characters;
-import org.apache.sis.util.logging.Logging;
-import org.apache.sis.util.resources.Vocabulary;
-
-import static java.util.logging.Logger.getLogger;
+import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.internal.gui.BackgroundThreads;
 
 
 /**
  * Provider of textual content to show in a {@link StatusBar} for values under 
cursor position.
- * When the mouse cursor moves, {@link #evaluate(DirectPosition)} is invoked 
with the same
+ * When the mouse cursor moves, {@link #evaluateLater(DirectPosition)} is 
invoked with the same
  * "real world" coordinates than the ones shown in the status bar.
  *
  * <h2>Multi-threading</h2>
- * Instances of {@code ValueUnderCursor} do not need to be thread-safe.
- * {@code ValuesUnderCursor} methods will be invoked from JavaFX thread.
+ * Instances of {@code ValueUnderCursor} do not need to be thread-safe, because
+ * all {@code ValuesUnderCursor} methods will be invoked from JavaFX thread.
+ * However the actual fetching and formatting of values will be done in a 
background
+ * thread using the {@link Formatter} inner class, which needs to be 
thread-safe.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.3
@@ -74,7 +46,8 @@ import static java.util.logging.Logger.getLogger;
 public abstract class ValuesUnderCursor {
     /**
      * The status bar for which this object is providing values.
-     * Each {@link ValuesUnderCursor} instance is used by at most {@link 
StatusBar} instance.
+     * Each {@link ValuesUnderCursor} instance is used by at most one {@link 
StatusBar} instance.
+     * This field shall be read and written from JavaFX thread only.
      *
      * @see #update(StatusBar, ValuesUnderCursor, ValuesUnderCursor)
      */
@@ -84,16 +57,52 @@ public abstract class ValuesUnderCursor {
      * Menu offering choices among the values that this {@code 
ValuesUnderCursor} can show.
      * This menu will be available as a contextual menu in the {@link 
StatusBar}.
      * It is subclass responsibility to listen to menu selections and adapt 
their
-     * {@link #evaluate(DirectPosition)} output accordingly.
+     * {@link #evaluateLater(DirectPosition)} output accordingly.
      */
     protected final Menu valueChoices;
 
     /**
-     * Message of the last exception, used for avoiding flooding the logger 
with repetitive errors.
-     *
-     * @see #recoverableException(String, Exception)
+     * The task to execute in JavaFX thread for showing the result of 
formatting values at cursor position.
+     * This is given in a call to {@link Platform#runLater(Runnable)} after 
the values have been formatted
+     * as text in a background thread.
      */
-    private String lastErrorMessage;
+    private final Consumer consumer;
+
+    /**
+     * Task to execute in JavaFX thread for showing the result of formatting 
values at cursor position.
+     * The {@link AtomicReference} value is the text to show in {@linkplain 
#owner owner} status bar.
+     * The value is atomically set to {@code null} as it is given to the 
control.
+     */
+    @SuppressWarnings("serial")         // Not intended to be serialized.
+    private final class Consumer extends AtomicReference<String> implements 
Runnable {
+        /**
+         * Creates a new task to execute in JavaFX thread for showing sample 
values.
+         */
+        Consumer() {
+        }
+
+        /**
+         * Sets the result to the given value, then submits a task in JavaFX 
thread if no task is already waiting.
+         * If a task is already waiting to be executed, then that task will 
use the specified value instead than
+         * the value which was specified when the previous task was submitted.
+         */
+        final void setLater(final String result) {
+            if (getAndSet(result) == null) {
+                Platform.runLater(this);
+            }
+        }
+
+        /**
+         * Invoked in JavaFX thread for showing the sample values. The value 
is reset to {@code null}
+         * for letting {@link #setLater(String)} know that the value has been 
consumed.
+         */
+        @Override
+        public void run() {
+            final String text = getAndSet(null);        // Must be invoked 
even if `owner` is null.
+            final StatusBar c = owner;
+            if (c != null) c.setSampleValues(text);
+        }
+    }
 
     /**
      * Creates a new evaluator instance. The {@link #valueChoices} list of 
items is initially empty;
@@ -101,519 +110,247 @@ public abstract class ValuesUnderCursor {
      */
     protected ValuesUnderCursor() {
         valueChoices = new Menu();
+        consumer = new Consumer();
     }
 
     /**
-     * Returns {@code true} if this {@code ValuesUnderCursor} has currently no 
data to show.
-     * A {@code ValuesUnderCursor} may be empty for example if user unselected 
all bands from
-     * the contextual menu.
+     * Returns the task for fetching and formatting values in a background 
thread.
+     * {@code ValuesUnderCursor} subclasses should keep a single {@link 
Formatter} instance,
+     * eventually replaced when the data shown in {@link MapCanvas} changed.
+     * That instance will be reused every time that the cursor position 
changed.
      *
-     * @return {@code true} if there is no data to show yet.
+     * @return task for fetching and formatting values in a background thread, 
or {@code null} if none.
+     *
+     * @since 1.3
      */
-    public abstract boolean isEmpty();
+    protected abstract Formatter formatter();
 
     /**
-     * Returns a string representation of data under given "real world" 
position.
-     * The {@linkplain DirectPosition#getCoordinateReferenceSystem() position 
CRS}
+     * Formats a string representation of data under given "real world" 
position.
+     * This method shall be invoked in JavaFX thread, but values will be 
fetched
+     * and formatted in a background thread managed automatically by this
+     * {@code ValuesUnderCursor} class.
+     *
+     * <p>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.
+     * The position CRS can be anything; it will be transformed if needed.</p>
      *
      * @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.
-     * @return string representation of data under given position, or {@code 
null} if none.
-     */
-    public abstract String evaluate(final DirectPosition point);
-
-    /**
-     * 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
-     * in the {@code others} argument).
-     *
-     * <p>If {@code main} is an empty string, then no values are expected and 
{@link MapCanvas}
-     * may hide the space normally used for showing values.</p>
      *
-     * @param  main    a prototype of longest normal text that we expect.
-     * @param  others  some other texts that may appear, such as labels for 
missing data.
-     * @return {@code true} on success, or {@code false} if this method should 
be invoked again.
-     */
-    final boolean prototype(final String main, final Iterable<String> others) {
-        return (owner == null) || owner.computeSizeOfSampleValues(main, 
others);
-    }
-
-    /**
-     * Invoked when {@link StatusBar#sampleValuesProvider} changed. Each 
{@link ValuesUnderCursor} instance
-     * can be used by at most one {@link StatusBar} instance. Current 
implementation silently does nothing
-     * if this is not the case.
+     * @since 1.3
      */
-    static void update(final StatusBar owner, final ValuesUnderCursor 
oldValue, final ValuesUnderCursor newValue) {
-        if (oldValue != null && oldValue.owner == owner) {
-            oldValue.owner = null;
-        }
-        if (newValue != null && newValue.owner == null) {
-            newValue.owner = owner;
+    public void evaluateLater(final DirectPosition point) {
+        final Formatter formatter = formatter();
+        if (formatter != null) {
+            formatter.evaluateLater(point);
         }
     }
 
     /**
-     * Creates a new instance for the given canvas and registers as a listener 
by weak reference.
-     * Caller must retain the returned reference somewhere, e.g. in {@link 
StatusBar#sampleValuesProvider}.
+     * Task for fetching and formatting values in a background thread.
+     * The background thread and the interaction with JavaFX thread are 
managed by the enclosing class.
+     * The same {@code Formatter} instance can be reused as long as the source 
of data does not change.
      *
-     * @param  canvas  the canvas for which to create a {@link 
ValuesUnderCursor}, or {@code null}.
-     * @return the sample values provider, or {@code null} if none.
-     */
-    static ValuesUnderCursor create(final MapCanvas canvas) {
-        if (canvas instanceof CoverageCanvas) {
-            final CoverageCanvas cc = (CoverageCanvas) canvas;
-            final FromCoverage listener = new FromCoverage();
-            cc.coverageProperty.addListener(new 
WeakChangeListener<>(listener));
-            cc.sliceExtentProperty.addListener((p,o,n) -> 
listener.setSlice(n));
-            final GridCoverage coverage = cc.coverageProperty.get();
-            if (coverage != null) {
-                listener.changed(null, null, coverage);
-                listener.setSlice(cc.getSliceExtent());
-            }
-            return listener;
-        } else {
-            // More cases may be added in the future.
-        }
-        return null;
-    }
-
-    /**
-     * Provider of textual content to show in {@link StatusBar} for {@link 
GridCoverage} values under cursor position.
-     * 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.
+     * <p>As a rule of thumbs, all properties in {@link ValuesUnderCursor} 
class shall be read and written
+     * from the JavaFX thread, while all properties in this {@code Formatter} 
class may be read and written
+     * from any thread.</p>
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @version 1.3
-     * @since   1.1
+     * @since   1.3
      * @module
      */
-    private static class FromCoverage extends ValuesUnderCursor implements 
ChangeListener<GridCoverage> {
-        /**
-         * The separator to insert between sample values. We use EM space.
-         */
-        private static final char SEPARATOR = '\u2003';
-
+    protected abstract static class Formatter implements Runnable {
         /**
-         * Pseudo amount of fraction digits for default format.
-         * Used when we don't know how many fraction digits to use.
-         */
-        private static final int DEFAULT_FORMAT = -1;
-
-        /**
-         * Pseudo amount of fraction digits for scientific notation.
-         */
-        private static final int SCIENTIFIC_NOTATION = -2;
-
-        /**
-         * The object computing or interpolation sample values in the coverage.
-         */
-        private GridCoverage.Evaluator evaluator;
-
-        /**
-         * The selection status of each band.
-         */
-        private final BitSet selectedBands;
-
-        /**
-         * Formatter for the values computed or interpolated by {@link 
#evaluator}.
-         * The number of fraction digits is computed from transfer function 
resolution.
-         * The same {@link NumberFormat} instance may appear at more than one 
index.
+         * Coordinates and CRS of the position where to evaluate values.
+         * This position shall not be modified; new coordinates shall be 
specified in a new instance.
+         * A {@code null} value means that there is no more sample values to 
format.
+         *
+         * <p>Instances are created by {@link #copy(DirectPosition)}. The same 
instance shall be given
+         * to {@link #evaluate(DirectPosition)} because subclasses may rely on 
a specific type.</p>
          */
-        private NumberFormat[] sampleFormats;
+        private DirectPosition position;
 
         /**
-         * Buffer where to format the textual content.
+         * Whether there is a new point for which to format sample values.
+         * The new point may be {@code null}.
          */
-        private final StringBuffer buffer;
+        private boolean hasNewPoint;
 
         /**
-         * Ignored but required by {@link NumberFormat}.
+         * Whether a background thread is already running. This information is 
used for looping
+         * in the running thread instead of launching many threads when 
coordinates are updated.
          */
-        private final FieldPosition field;
+        private boolean isRunning;
 
         /**
-         * Unit symbol to write after each value.
+         * A copy of {@link ValuesUnderCursor#consumer} field.
          */
-        private String[] units;
+        private final Consumer consumer;
 
         /**
-         * The text to show when value under cursor is a NaN value.
-         * Values are packed with band number in low bits and float ordinal 
value in high bits.
+         * Creates a new formatter instance.
          *
-         * @see #toNodataKey(int, float)
-         * @see MathFunctions#toNanOrdinal(float)
-         */
-        private final Map<Long,String> nodata;
-
-        /**
-         * The text to show when cursor is outside coverage area. It should 
contain dimension names,
-         * for example "(SST)". May be {@code null} if {@link 
#onBandSelectionChanged()} needs to be invoked.
-         */
-        private String outsideText;
-
-        /**
-         * {@code true} if {@link #onBandSelectionChanged()} needs to be 
invoked again.
-         */
-        private boolean needsBandRefresh;
-
-        /**
-         * Creates a new provider of textual values for a {@link GridCoverage}.
+         * @param  owner  instance of the enclosing class which will evaluate 
values under cursor position.
          */
-        public FromCoverage() {
-            buffer        = new StringBuffer();
-            field         = new FieldPosition(0);
-            nodata        = new HashMap<>();
-            selectedBands = new BitSet();
-            
valueChoices.setText(Vocabulary.format(Vocabulary.Keys.SampleDimensions));
+        protected Formatter(final ValuesUnderCursor owner) {
+            consumer = owner.consumer;
         }
 
         /**
-         * Returns {@code true} if all bands are unselected.
+         * Invoked in JavaFX thread for creating a copy of the given position.
+         * A copy is needed because the position will be read in a background 
thread,
+         * and the {@code point} instance may change concurrently.
+         *
+         * <p>Subclasses can override this method for opportunistically 
fetching
+         * in JavaFX thread other information related to the current cursor 
position.
+         * Those information can be stored in a custom {@link DirectPosition} 
implementation class.
+         * The {@link DirectPosition} instance given to the {@link 
#evaluate(DirectPosition)} method
+         * will be the instance returned by this method.</p>
+         *
+         * @param  point  position to copy (never {@code null}).
+         * @return a copy of the given position, or {@code null} if the 
position should be considered outside.
          */
-        @Override
-        public boolean isEmpty() {
-            return selectedBands.isEmpty();
+        DirectPosition copy(final DirectPosition point) {
+            return new GeneralDirectPosition(point);
         }
 
         /**
-         * Returns the canvas which contains the given property.
+         * Sets the position of next point to evaluate, then launches 
background thread if not already running.
+         * Even if technically this method can be invoked from any thread, it 
should be the JavaFX thread.
+         * The given position will be copied in order to protect it from 
concurrent changes.
+         *
+         * @param  point  coordinates of the point for which to evaluate the 
grid coverage value.
+         *                May be {@code null} for declaring that the point is 
outside canvas region.
+         *
+         * @see ValuesUnderCursor#evaluateLater(DirectPosition)
          */
-        private static Optional<CoverageCanvas> canvas(final 
ObservableValue<?> property) {
-            if (property instanceof ReadOnlyProperty<?>) {
-                final Object bean = ((ReadOnlyProperty<?>) property).getBean();
-                if (bean instanceof CoverageCanvas) {
-                    return Optional.of((CoverageCanvas) bean);
-                }
+        final synchronized void evaluateLater(final DirectPosition point) {
+            position = (point != null) ? copy(point) : null;
+            hasNewPoint = true;
+            if (!isRunning) {
+                BackgroundThreads.execute(this);
+                isRunning = true;                   // Set only after success.
             }
-            return Optional.empty();
         }
 
         /**
-         * Notifies this {@code ValuesUnderCursor} object that it needs to 
display values for a new coverage.
-         * The {@code previous} argument should be the argument given in the 
last call to this method and is
-         * used as an optimization hint. In case of doubt, it can be {@code 
null}.
+         * Invoked in a background thread for formatting values at the most 
recent position.
+         * If the cursor moves while this method is formatting values, then 
this method will
+         * continue its execution for formatting also the values at new 
positions until the
+         * cursor stop moving.
          *
-         * @param  property  the property which has been updated, or {@code 
null} if unknown.
-         * @param  previous  previous property value, of {@code null} if none 
or unknown.
-         * @param  coverage  new coverage for which to show sample values, or 
{@code null} if none.
+         * <p>This method does not need to be invoked explicitly; it is 
invoked automatically
+         * by {@link ValuesUnderCursor}. But it may be overridden for adding 
pretreatment or
+         * post-treatment.</p>
          */
         @Override
-        public void changed(final ObservableValue<? extends GridCoverage> 
property,
-                            final GridCoverage previous, final GridCoverage 
coverage)
-        {
-            final List<SampleDimension> bands;      // Should never be null, 
but check anyway.
-            if (coverage == null || (bands = coverage.getSampleDimensions()) 
== null) {
-                evaluator     = null;
-                units         = null;
-                sampleFormats = null;
-                outsideText   = null;
-                nodata.clear();
-                selectedBands.clear();
-                valueChoices.getItems().clear();
-                return;
-            }
-            evaluator = coverage.forConvertedValues(true).evaluator();
-            evaluator.setNullIfOutside(true);
-            evaluator.setWraparoundEnabled(true);
-            canvas(property).ifPresent((c) -> setSlice(c.getSliceExtent()));
-            if (previous != null && 
bands.equals(previous.getSampleDimensions())) {
-                // Same configuration than previous coverage.
-                return;
-            }
-            final int numBands = bands.size();
-            units         = new String[numBands];
-            sampleFormats = new NumberFormat[numBands];
-            outsideText   = null;               // Will be recomputed on next 
`evaluate(…)` call.
-            /*
-             * Only the first band is initially selected, unless the image has 
only 2 or 3 bands
-             * in which case all bands are selected. An image with two bands 
is often giving the
-             * (u,v) components of velocity vectors, which we want to keep 
together by default.
-             */
-            selectedBands.clear();
-            selectedBands.set(0, (numBands <= 3) ? numBands : 1, true);
-            nodata.clear();
-            /*
-             * Loop below initializes number formats and unit symbols for all 
bands, regardless
-             * if selected or not. We do that on the assumption that the same 
format and symbol
-             * are typically shared by all bands.
-             */
-            final Map<Integer,NumberFormat> sharedFormats = new HashMap<>();
-            final Map<Unit<?>,String>       sharedSymbols = new HashMap<>();
-            final Locale                    locale        = 
GUIUtilities.getLocale(property);
-            final UnitFormat                unitFormat    = new 
UnitFormat(locale);
-            final CheckMenuItem[]           menuItems     = new 
CheckMenuItem[numBands];
-            for (int b=0; b<numBands; b++) {
-                final SampleDimension sd = bands.get(b);
-                menuItems[b] = createMenuItem(b, sd, locale);
-                /*
-                 * Build the list of texts to show for missing values. A 
coverage can have
-                 * different NaN values representing different kind of missing 
values.
-                 */
-                for (final Category c : 
sd.forConvertedValues(true).getCategories()) {
-                    final float value = ((Number) 
c.getSampleRange().getMinValue()).floatValue();
-                    if (Float.isNaN(value)) try {
-                        nodata.putIfAbsent(toNodataKey(b, value), 
c.getName().toString(locale));
-                    } catch (IllegalArgumentException e) {
-                        recoverableException("changed", e);
+        public void run() {
+            for (;;) {                              // `while(hasNewPoint)` 
but synchronized.
+                final DirectPosition point;
+                synchronized (this) {
+                    if (!hasNewPoint) {
+                        isRunning = false;          // Must be inside the 
synchronized block.
+                        break;
                     }
+                    point       = position;
+                    position    = null;
+                    hasNewPoint = false;
                 }
-                /*
-                 * Format in advance the units of measurement. If none, an 
empty string is used.
-                 * Note: it is quite common that all bands use the same unit 
of measurement.
-                 */
-                units[b] = sd.getUnits().map((unit) -> 
sharedSymbols.computeIfAbsent(unit,
-                                              (key) -> format(unitFormat, 
key))).orElse("");
-                /*
-                 * Infer a number of fraction digits to use for the resolution 
of sample values in each band.
-                 */
-                final SampleDimension isd = sd.forConvertedValues(false);
-                final Integer nf = isd.getTransferFunctionFormula().map(
-                        (formula) -> suggestFractionDigits(formula, 
isd)).orElse(DEFAULT_FORMAT);
-                /*
-                 * Create number formats with a number of fraction digits 
inferred from sample value resolution.
-                 * The same format instances are shared when possible. Keys 
are the number of fraction digits.
-                 * Special values:
-                 *
-                 *   - Key  0 is for integer values.
-                 *   - Key -1 is for default format with unspecified number of 
fraction digits.
-                 *   - Key -2 is for scientific notation.
-                 */
-                sampleFormats[b] = sharedFormats.computeIfAbsent(nf, 
(precision) -> {
-                    switch (precision) {
-                        case 0:              return 
NumberFormat.getIntegerInstance(locale);
-                        case DEFAULT_FORMAT: return 
NumberFormat.getNumberInstance(locale);
-                        case SCIENTIFIC_NOTATION: {
-                            final NumberFormat format = 
NumberFormat.getNumberInstance(locale);
-                            if (precision == SCIENTIFIC_NOTATION && format 
instanceof DecimalFormat) {
-                                ((DecimalFormat) 
format).applyPattern("0.000E00");
-                            }
-                            return format;
-                        }
-                        default: {
-                            final NumberFormat format = 
NumberFormat.getNumberInstance(locale);
-                            format.setMinimumFractionDigits(precision);
-                            format.setMaximumFractionDigits(precision);
-                            return format;
-                        }
-                    }
-                });
+                consumer.setLater(evaluate(point));
             }
-            valueChoices.getItems().setAll(menuItems);
-            onBandSelectionChanged();
         }
 
         /**
-         * Returns the key to use in {@link #nodata} map for the given "no 
data" value.
-         * The band number can be obtained by {@link Long#intValue()}.
+         * Returns a string representation of data under the 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  band   band index.
-         * @param  value  the NaN value used for "no data".
-         * @return key to use in {@link #nodata} map.
-         * @throws IllegalArgumentException if the given value is not a NaN 
value
-         *         or does not use a supported bits pattern.
-         */
-        private static Long toNodataKey(final int band, final float value) {
-            return (((long) MathFunctions.toNanOrdinal(value)) << 
Integer.SIZE) | band;
-        }
-
-        /**
-         * Suggests a number of fraction digits for numbers formatted after 
conversion by the given formula.
-         * This is either a positive number (including 0 for integers), or the 
{@value #SCIENTIFIC_NOTATION}
-         * or {@value #DEFAULT_FORMAT} sentinel values.
-         */
-        private static Integer suggestFractionDigits(final TransferFunction 
formula, final SampleDimension isd) {
-            int nf;
-            if (formula.getType() != TransferFunctionType.LINEAR) {
-                nf = SCIENTIFIC_NOTATION;
-            } else {
-                double resolution = formula.getScale();
-                if (resolution > 0 && resolution <= Double.MAX_VALUE) {     // 
Non-zero, non-NaN and finite.
-                    final Optional<NumberRange<?>> range = 
isd.getSampleRange();
-                    if (range.isPresent()) {
-                        // See StatusBar.inflatePrecisions for rationale.
-                        resolution *= (0.5 / range.get().getSpan()) + 1;
-                    }
-                    nf = DecimalFunctions.fractionDigitsForDelta(resolution, 
false);
-                    if (nf < -9 || nf > 6) nf = SCIENTIFIC_NOTATION;        // 
Arbitrary thresholds.
-                } else {
-                    nf = DEFAULT_FORMAT;
-                }
-            }
-            return nf;
-        }
-
-        /**
-         * Creates a new menu item for the given sample dimension.
+         * <p>This method is invoked by {@link #run()} in a background thread.
+         * Implementations are responsible for fetching data in a thread-safe 
manner.</p>
          *
-         * @param  index   index of the sample dimension.
-         * @param  sd      the sample dimension for which to create a menu 
item.
-         * @param  locale  the locale to use for fetching the sample dimension 
name.
+         * @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.
+         * @return string representation of data under given position, or 
{@code null} if none.
          */
-        private CheckMenuItem createMenuItem(final int index, final 
SampleDimension sd, final Locale locale) {
-            final CheckMenuItem item = new 
CheckMenuItem(sd.getName().toInternationalString().toString(locale));
-            item.setSelected(selectedBands.get(index));
-            item.selectedProperty().addListener((p,o,n) -> {
-                selectedBands.set(index, n);
-                onBandSelectionChanged();
-            });
-            return item;
-        }
+        public abstract String evaluate(final DirectPosition point);
+    }
 
-        /**
-         * Tells to the evaluator in which slice to evaluate coordinates.
-         * This method is invoked when {@link 
CoverageCanvas#sliceExtentProperty} changed its value.
-         */
-        final void setSlice(final GridExtent extent) {
-            if (evaluator != null) {
-                evaluator.setDefaultSlice(extent != null ? 
extent.getSliceCoordinates() : null);
-            }
-        }
+    /**
+     * Returns whether a status bar is associated to this instance.
+     * If {@code false}, then it is useless to compute values for {@link 
#prototype(String, Iterable)}.
+     */
+    final boolean usePrototype() {
+        return owner != null;
+    }
 
-        /**
-         * Returns a string representation of data under given position.
-         * The position may be in any CRS; this method will convert 
coordinates as needed.
-         *
-         * @param  point  the cursor location in arbitrary CRS, or {@code 
null} if outside canvas region.
-         * @return string representation of data under given position, or 
{@code null} if none.
-         *
-         * @see GridCoverage.Evaluator#apply(DirectPosition)
-         */
-        @Override
-        public String evaluate(final DirectPosition point) {
-            if (needsBandRefresh && evaluator != null) {
-                onBandSelectionChanged();
-            }
-            if (point != null) {
-                /*
-                 * Take lock once instead of at each StringBuffer method call. 
It makes this method thread-safe,
-                 * but this is a side effect of the fact that `NumberFormat` 
accepts only `StringBuffer` argument.
-                 * We do not document this thread-safety in method contract 
since it is not guaranteed to apply in
-                 * future SIS versions if a future `NumberFormat` version 
accepts non-synchronized `StringBuilder`.
-                 */
-                synchronized (buffer) {
-                    buffer.setLength(0);
-                    if (evaluator != null) try {
-                        final double[] results = evaluator.apply(point);
-                        if (results != null) {
-                            for (int i = -1; (i = 
selectedBands.nextSetBit(i+1)) >= 0;) {
-                                if (buffer.length() != 0) {
-                                    buffer.append(SEPARATOR);
-                                }
-                                final double value = results[i];
-                                if (Double.isNaN(value)) try {
-                                    /*
-                                     * If a value is NaN, returns its label as 
the whole content. Numerical values
-                                     * in other bands are lost. We do that 
because "no data" strings are often too
-                                     * long for being shown together with 
numerical values, and are often the same
-                                     * for all bands. Users can see numerical 
values by hiding the band containing
-                                     * "no data" values with contextual menu 
on the status bar.
-                                     */
-                                    final String label = 
nodata.get(toNodataKey(i, (float) value));
-                                    if (label != null) return label;
-                                } catch (IllegalArgumentException e) {
-                                    recoverableException("evaluate", e);
-                                }
-                                sampleFormats[i].format(value, buffer, 
field).append(units[i]);
-                            }
-                            return buffer.toString();
-                        }
-                    } catch (CannotEvaluateException e) {
-                        recoverableException("evaluate", e);
-                    }
-                }
-            }
-            /*
-             * Coordinate is considered outside coverage area.
-             * Format the sample dimension names.
-             */
-            return outsideText;
-        }
+    /**
+     * 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
+     * in the {@code others} argument).
+     *
+     * <p>If {@code main} is an empty string, then no values are expected and 
{@link MapCanvas}
+     * may hide the space normally used for showing values.</p>
+     *
+     * @param  main    a prototype of longest normal text that we expect.
+     * @param  others  some other texts that may appear, such as labels for 
missing data.
+     * @return {@code true} on success, or {@code false} if this method should 
be invoked again.
+     */
+    final boolean prototype(final String main, final Iterable<String> others) {
+        return (owner == null) || owner.computeSizeOfSampleValues(main, 
others);
+    }
 
-        /**
-         * Formats the unit symbol to append after a sample value. The unit 
symbols are created in advance
-         * and reused for all sample value formatting as long as the sample 
dimensions do not change.
-         */
-        private String format(final UnitFormat format, final Unit<?> unit) {
-            synchronized (buffer) {         // Take lock once instead of at 
each StringBuffer method call.
-                buffer.setLength(0);
-                format.format(unit, buffer, field);
-                if (buffer.length() != 0 && 
Character.isLetterOrDigit(buffer.codePointAt(0))) {
-                    buffer.insert(0, Characters.NO_BREAK_SPACE);
-                }
-                return buffer.toString();
+    /**
+     * Invoked when {@link StatusBar#sampleValuesProvider} changed. Each 
{@link ValuesUnderCursor} instance
+     * can be used by at most one {@link StatusBar} instance. Current 
implementation silently does nothing
+     * if this is not the case.
+     */
+    static void update(final StatusBar owner, final ValuesUnderCursor 
oldValue, final ValuesUnderCursor newValue) {
+        if (oldValue != null && oldValue.owner == owner) {
+            oldValue.owner = null;
+        }
+        if (newValue != null && newValue.owner != owner) {
+            if (newValue.owner != null) {
+                newValue.owner.sampleValuesProvider.set(null);
             }
+            newValue.owner = owner;
         }
+    }
 
-        /**
-         * Formats the widest text that we expect. This text is used for 
computing the label width.
-         * Also computes the text to show when cursor is outside coverage 
area. This method is invoked
-         * when the bands selection changed, either because of selection in 
contextual menu or because
-         * {@link ValuesUnderCursor} is providing data for a new coverage.
-         *
-         * <p>We use {@link #needsBandRefresh} as a flag meaning meaning that 
this method needs
-         * to be invoked. This method invocation sometime needs to be delayed 
because calculation of
-         * text width may be wrong (produce 0 values) if invoked before {@link 
StatusBar#sampleValues}
-         * label is added in the scene graph.</p>
-         */
-        private void onBandSelectionChanged() {
-            final ObservableList<MenuItem> menus = valueChoices.getItems();
-            final List<SampleDimension>    bands = 
evaluator.getCoverage().getSampleDimensions();
-            final StringBuilder            names = new 
StringBuilder().append('(');
-            final String text;
-            synchronized (buffer) {
-                buffer.setLength(0);
-                for (int i = -1; (i = selectedBands.nextSetBit(i+1)) >= 0;) {
-                    if (buffer.length() != 0) {
-                        buffer.append(SEPARATOR);
-                        names.append(", ");
-                    }
-                    names.append(menus.get(i).getText());
-                    final int start = buffer.length();
-                    final Comparable<?>[] sampleValues = 
bands.get(i).forConvertedValues(true)
-                            .getSampleRange().map((r) -> new Comparable<?>[] 
{r.getMinValue(), r.getMaxValue()})
-                            .orElseGet(() -> new Comparable<?>[] {0xFFFF});    
             // Arbitrary value.
-                    for (final Comparable<?> value : sampleValues) {
-                        final int end = buffer.length();
-                        sampleFormats[i].format(value, buffer, field);
-                        final int length = buffer.length();
-                        if (length - end >= end - start) {
-                            buffer.delete(start, end);      // Delete first 
number if it was shorter.
-                        } else {
-                            buffer.setLength(end);          // Delete second 
number if it is shorter.
-                        }
-                    }
-                    buffer.append(units[i]);
-                }
-                text = buffer.toString();
-            }
-            /*
-             * At this point, `text` is the longest string of numerical values 
that we expect.
-             * We also need to take in account the width required for 
displaying "no data" labels.
-             * If a "no data" label is shown, it will be shown alone (we do 
not need to compute a
-             * sum of "no data" label widths).
-             */
-            outsideText = text.isEmpty() ? "" : names.append(')').toString();
-            final HashSet<String> others = new HashSet<>();
-            for (final Map.Entry<Long,String> other : nodata.entrySet()) {
-                if (selectedBands.get(other.getKey().intValue())) {
-                    others.add(other.getValue());
-                }
+    /**
+     * Creates a new instance for the given canvas and registers as a listener 
by weak reference.
+     * Caller must retain the returned reference somewhere, e.g. in {@link 
StatusBar#sampleValuesProvider}.
+     *
+     * @param  canvas  the canvas for which to create a {@link 
ValuesUnderCursor}, or {@code null}.
+     * @return the sample values provider, or {@code null} if none.
+     */
+    static ValuesUnderCursor create(final MapCanvas canvas) {
+        if (canvas instanceof CoverageCanvas) {
+            final CoverageCanvas cc = (CoverageCanvas) canvas;
+            final ValuesFromCoverage listener = new ValuesFromCoverage();
+            cc.coverageProperty.addListener(new 
WeakChangeListener<>(listener));
+            cc.sliceExtentProperty.addListener((p,o,n) -> 
listener.setSlice(n));
+            final GridCoverage coverage = cc.coverageProperty.get();
+            if (coverage != null) {
+                listener.changed(null, null, coverage);
             }
-            needsBandRefresh = !prototype(text, others);
+            return listener;
+        } else {
+            // More cases may be added in the future.
         }
+        return null;
     }
 
     /**
      * Invoked when an exception occurred while computing values.
      */
-    final void recoverableException(final String method, final Exception e) {
-        final String message = e.getMessage();
-        if (!message.equals(lastErrorMessage)) {
-            lastErrorMessage = message;
-            Logging.recoverableException(getLogger(Modules.APPLICATION), 
ValuesUnderCursor.class, method, e);
+    final void setError(final Throwable e) {
+        final StatusBar owner = this.owner;
+        if (owner != null) {
+            owner.setSampleValues(owner.cause(e));
         }
     }
 }


Reply via email to