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: Replace the "Image operation" pane by an "Image properties" pane. We are not ready to offer image operations in the JavaFX applications. Instead image sources, layout and properties are more useful information, with the previous "positional errors" operation shown as an image property.
Date Mon, 22 Jun 2020 18:45:32 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 998181d  Replace the "Image operation" pane by an "Image properties" pane. We are not ready to offer image operations in the JavaFX applications. Instead image sources, layout and properties are more useful information, with the previous "positional errors" operation shown as an image property.
998181d is described below

commit 998181dacb555b1910df5589b86d36caba2ea366
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Jun 22 20:35:39 2020 +0200

    Replace the "Image operation" pane by an "Image properties" pane.
    We are not ready to offer image operations in the JavaFX applications.
    Instead image sources, layout and properties are more useful information,
    with the previous "positional errors" operation shown as an image property.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    | 108 ++-
 .../apache/sis/gui/coverage/CoverageControls.java  |  59 +-
 .../apache/sis/gui/coverage/ImageDerivative.java   |  97 ---
 .../apache/sis/gui/coverage/ImageOperation.java    | 147 ----
 .../sis/gui/coverage/ImagePropertyExplorer.java    | 822 +++++++++++++++++++++
 .../org/apache/sis/gui/coverage/RenderingData.java |  43 +-
 .../apache/sis/gui/coverage/StatusBarSupport.java  | 228 ------
 .../java/org/apache/sis/gui/map/MapCanvas.java     |   7 +-
 .../org/apache/sis/internal/gui/GUIUtilities.java  |  16 +
 .../apache/sis/internal/gui/ImageConverter.java    | 156 ++++
 .../sis/internal/gui/ImmutableObjectProperty.java  | 112 +++
 .../org/apache/sis/internal/gui/PropertyView.java  | 300 ++++++++
 .../org/apache/sis/internal/gui/Resources.java     |  45 +-
 .../apache/sis/internal/gui/Resources.properties   |   9 +-
 .../sis/internal/gui/Resources_fr.properties       |   9 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |  30 +-
 .../sis/util/resources/Vocabulary.properties       |   6 +-
 .../sis/util/resources/Vocabulary_fr.properties    |   6 +-
 18 files changed, 1606 insertions(+), 594 deletions(-)

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 764ee70..01e6bbc 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
@@ -17,12 +17,13 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.Map;
-import java.util.HashMap;
+import java.util.EnumMap;
 import java.util.Locale;
 import java.awt.Graphics2D;
+import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
 import java.awt.geom.AffineTransform;
-import javafx.scene.control.ListView;
+import java.awt.geom.NoninvertibleTransformException;
 import javafx.scene.paint.Color;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.Background;
@@ -43,16 +44,18 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.ImageRenderer;
 import org.apache.sis.internal.gui.ExceptionReporter;
+import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.geometry.AbstractEnvelope;
 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;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.logging.Logging;
 
 
 /**
@@ -108,22 +111,20 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * The {@link #data} resampled to a CRS which can easily be mapped to {@linkplain #getDisplayCRS() display CRS}.
-     * The different values are variants of the values associated to {@link ImageDerivative#NONE}, with color ramp
-     * changed or other operation applied.
+     * The different values are variants with color ramp changed.
      */
-    private final Map<ImageDerivative,RenderedImage> resampledImages;
+    private final Map<Stretching,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.
+     * The explorer to notify when the image shown in this canvas has changed.
+     * This is non-null only if this {@link CoverageCanvas} is used together with {@link CoverageControls}.
      *
-     * <p>Consider as final after {@link #initialize(StatusBar, ListView)} 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>
+     * <p>Consider as final after {@link #createPropertyExplorer()} invocation.
+     * This field may be removed in a future version if we revisit this API before making public.</p>
      *
-     * @see #initialize(StatusBar, ListView)
+     * @see #createPropertyExplorer
      */
-    private StatusBarSupport statusBar;
+    private ImagePropertyExplorer imageProperty;
 
     /**
      * Creates a new two-dimensional canvas for {@link RenderedImage}.
@@ -131,7 +132,7 @@ public class CoverageCanvas extends MapCanvasAWT {
     public CoverageCanvas() {
         super(Locale.getDefault());
         data                  = new RenderingData();
-        resampledImages       = new HashMap<>();
+        resampledImages       = new EnumMap<>(Stretching.class);
         coverageProperty      = new SimpleObjectProperty<>(this, "coverage");
         sliceExtentProperty   = new SimpleObjectProperty<>(this, "sliceExtent");
         interpolationProperty = new SimpleObjectProperty<>(this, "interpolation", data.getInterpolation());
@@ -141,14 +142,15 @@ 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.
+     * Completes initialization of this canvas for use with the returned property explorer.
+     * The intent is to be notified when the image used for showing the coverage changed.
+     * This method may be removed in a future SIS version if we revisit this API before
+     * to make public.
      */
-    final void initialize(final StatusBar bar, final ListView<ImageOperation> operations) {
-        statusBar = new StatusBarSupport(bar, operations);
+    final ImagePropertyExplorer createPropertyExplorer() {
+        imageProperty = new ImagePropertyExplorer(getLocale(), fixedPane.backgroundProperty());
+        imageProperty.setImage(resampledImages.get(data.selectedDerivative), getVisibleImageBounds());
+        return imageProperty;
     }
 
     /**
@@ -396,6 +398,21 @@ public class CoverageCanvas extends MapCanvasAWT {
         }
 
         /**
+         * Returns the bounds of the image part which is currently shown. This method can be invoked
+         * only after {@link #render()}. It returns {@code null} if the visible bounds are unknown.
+         *
+         * @see CoverageCanvas#getVisibleImageBounds()
+         */
+        final Rectangle getVisibleImageBounds() {
+            try {
+                return (Rectangle) AffineTransforms2D.inverseTransform(resampledToDisplay, displayBounds, new Rectangle());
+            } catch (NoninvertibleTransformException e) {
+                unexpectedException(e);                     // Should never happen.
+            }
+            return null;
+        }
+
+        /**
          * Invoked in background thread for resampling the image or stretching the color ramp.
          * This method performs some of the steps documented in class Javadoc, with possibility
          * to skip the first step if the required source image is already resampled.
@@ -430,8 +447,8 @@ public class CoverageCanvas extends MapCanvasAWT {
         }
 
         /**
-         * Invoked in JavaFX thread after {@link #paint(Graphics2D)} completion. This method stores
-         * the computation results.
+         * Invoked in JavaFX thread after {@link #paint(Graphics2D)} completion.
+         * This method stores the computation results.
          */
         @Override
         protected boolean commit(final MapCanvas canvas) {
@@ -447,37 +464,40 @@ public class CoverageCanvas extends MapCanvasAWT {
     private void cacheRenderingData(final Worker worker) {
         data = worker.data;
         final RenderedImage newValue = worker.resampledImage;
-        final RenderedImage oldValue = resampledImages.put(ImageDerivative.NONE, newValue);
+        final RenderedImage oldValue = resampledImages.put(Stretching.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(Stretching.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.
+         * Notify the "Image properties" tab that the image changed.
          */
-        if (statusBar != null) {
-            statusBar.notifyImageShown(data.selectedDerivative.operation, worker.filteredImage);
+        if (imageProperty != null) {
+            imageProperty.setImage(worker.filteredImage, worker.getVisibleImageBounds());
         }
     }
 
     /**
-     * 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)}.
+     * Returns the bounds of the image part which is currently shown. This method performs the same work
+     * than {@link Worker#getVisibleImageBounds()} is a less efficient way. It is used when no worker is
+     * available.
+     *
+     * @see Worker#getVisibleImageBounds()
      */
-    final void setOperation(final ImageOperation selection) {
-        final ImageDerivative sd = data.selectedDerivative;
-        data.selectedDerivative = sd.setOperation(selection);
-        if (data.selectedDerivative != sd) {
-            requestRepaint();
+    private Rectangle getVisibleImageBounds() {
+        final Envelope2D displayBounds = getDisplayBounds();
+        final AffineTransform resampledToDisplay = data.getTransform(getObjectiveToDisplay());
+        try {
+            return (Rectangle) AffineTransforms2D.inverseTransform(resampledToDisplay, displayBounds, new Rectangle());
+        } catch (NoninvertibleTransformException e) {
+            unexpectedException(e);                     // Should never happen.
         }
+        return null;
     }
 
     /**
@@ -485,9 +505,8 @@ public class CoverageCanvas extends MapCanvasAWT {
      * The sample values are assumed the same; only the image appearance is modified.
      */
     final void setStyling(final Stretching selection) {
-        final ImageDerivative sd = data.selectedDerivative;
-        data.selectedDerivative = sd.setStyling(selection);
-        if (data.selectedDerivative != sd) {
+        if (data.selectedDerivative != selection) {
+            data.selectedDerivative = selection;
             requestRepaint();
         }
     }
@@ -522,6 +541,13 @@ public class CoverageCanvas extends MapCanvasAWT {
     }
 
     /**
+     * Invoked when an exception occurred while computing a transform but the painting process can continue.
+     */
+    private static void unexpectedException(final Exception e) {
+        Logging.unexpectedException(Logging.getLogger(Modules.APPLICATION), CoverageCanvas.class, "render", e);
+    }
+
+    /**
      * Removes the image shown and releases memory.
      */
     @Override
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 ca5f80b..eb46b51 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
@@ -29,8 +29,9 @@ import javafx.scene.layout.GridPane;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.VBox;
 import javafx.beans.property.ObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
 import javafx.scene.control.ChoiceBox;
-import javafx.scene.control.ListView;
 import javafx.scene.paint.Color;
 import javafx.util.StringConverter;
 import org.opengis.referencing.ReferenceSystem;
@@ -39,7 +40,6 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.image.Interpolation;
-import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.util.resources.Vocabulary;
 
 
@@ -119,30 +119,16 @@ final class CoverageControls extends Controls implements PropertyChangeListener
                 labelOfGroup(vocabulary, Vocabulary.Keys.Colors,          colors,     false), colors);
         }
         /*
-         * "Operation" section with the following controls:
-         *    - List of predefined operations.
-         */
-        final VBox operationPane;
-        {   // Block for making variables locale to this scope.
-            final Resources resources = Resources.forLocale(vocabulary.getLocale());
-            final ListView<ImageOperation> operations = new ListView<>();
-            operations.getSelectionModel().selectedItemProperty().addListener((p,o,n) -> view.setOperation(n));
-            operationPane = new VBox(
-                labelOfGroup(resources, Resources.Keys.PredefinedFilters, operations, true), operations);
-
-            view.initialize(statusBar, operations);
-        }
-        /*
          * Put all sections together and have the first one expanded by default.
+         * The "Properties" section will be built by `PropertyPaneCreator` only if requested.
          */
-        controls = new Accordion(
-            new TitledPane(vocabulary.getString(Vocabulary.Keys.Display),    displayPane),
-            new TitledPane(vocabulary.getString(Vocabulary.Keys.Operations), operationPane)
-            // TODO: more controls to be added in a future version.
-        );
-        controls.setExpandedPane(controls.getPanes().get(0));
+        final TitledPane p1 = new TitledPane(vocabulary.getString(Vocabulary.Keys.Display), displayPane);
+        final TitledPane p2 = new TitledPane(vocabulary.getString(Vocabulary.Keys.Properties), null);
+        controls = new Accordion(p1, p2);
+        controls.setExpandedPane(p1);
         view.coverageProperty.bind(coverage);
         view.addPropertyChangeListener(CoverageCanvas.OBJECTIVE_CRS_PROPERTY, this);
+        p2.expandedProperty().addListener(new PropertyPaneCreator(view, p2));
     }
 
     /**
@@ -222,6 +208,35 @@ final class CoverageControls extends Controls implements PropertyChangeListener
     }
 
     /**
+     * Invoked the first time that the "Properties" pane is opened for building the JavaFX visual components.
+     * We deffer the creation of this pane because it is often not requested at all, since this is more for
+     * developers than users.
+     */
+    private static final class PropertyPaneCreator implements ChangeListener<Boolean> {
+        /** A copy of {@link CoverageControls#view} reference. */
+        private final CoverageCanvas view;
+
+        /** The pane where to set the content. */
+        private final TitledPane pane;
+
+        /** Creates a new {@link ImagePropertyExplorer} constructor. */
+        PropertyPaneCreator(final CoverageCanvas view, final TitledPane pane) {
+            this.view = view;
+            this.pane = pane;
+        }
+
+        /** Creates the {@link ImagePropertyExplorer}. */
+        @Override public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue) {
+            if (newValue) {
+                pane.expandedProperty().removeListener(this);
+                final ImagePropertyExplorer properties = view.createPropertyExplorer();
+                properties.updateOnChange.bind(pane.expandedProperty());
+                pane.setContent(properties.getView());
+            }
+        }
+    }
+
+    /**
      * Invoked in JavaFX thread after {@link CoverageExplorer#setCoverage(ImageRequest)} completed.
      * This method updates the GUI with new information available.
      *
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageDerivative.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageDerivative.java
deleted file mode 100644
index fbb5f8e..0000000
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageDerivative.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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 org.apache.sis.internal.util.Strings;
-
-
-/**
- * Information about how to render an image.
- * This is a combination of {@link ImageOperation} with {@link Stretching}.
- * Note that {@link Stretching} is a temporary enumeration to be deleted after SIS provides styling support.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
- * @module
- */
-final class ImageDerivative {
-    /**
-     * The key when no operation is applied on the image.
-     */
-    static final ImageDerivative NONE = new ImageDerivative(ImageOperation.NONE, Stretching.NONE);
-
-    /**
-     * The operation applied on the image.
-     */
-    final ImageOperation operation;
-
-    /**
-     * Key of the currently selected alternative in {@link CoverageCanvas#resampledImages} map.
-     */
-    final Stretching styling;
-
-    /**
-     * Creates a new combination of operation and styling.
-     */
-    private ImageDerivative(final ImageOperation operation, final Stretching styling) {
-        this.operation = operation;
-        this.styling   = styling;
-    }
-
-    /**
-     * Returns a key with the same styling than this key but a different operation.
-     */
-    final ImageDerivative setOperation(final ImageOperation selection) {
-        return (selection != operation) ? new ImageDerivative(selection, styling) : this;
-    }
-
-    /**
-     * Returns a key with the same operation than this key but a different styling.
-     */
-    final ImageDerivative setStyling(final Stretching selection) {
-        return (selection != styling) ? new ImageDerivative(operation, selection) : this;
-    }
-
-    /**
-     * Compares this key with given object for equality.
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        if (obj instanceof ImageDerivative) {
-            final ImageDerivative other = (ImageDerivative) obj;
-            return (operation == other.operation) && (styling == other.styling);
-        }
-        return false;
-    }
-
-    /**
-     * Returns a hash code value for this key.
-     */
-    @Override
-    public int hashCode() {
-        return operation.hashCode() + 11 * styling.hashCode();
-    }
-
-    /**
-     * Returns a string representation for debugging purpose.
-     */
-    @Override
-    public String toString() {
-        return Strings.toString(getClass(), "operation", operation, "styling", styling);
-    }
-}
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
deleted file mode 100644
index 12d7b54..0000000
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageOperation.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.List;
-import java.util.Arrays;
-import java.util.EnumSet;
-import java.awt.image.RenderedImage;
-import javafx.scene.control.ListView;
-import javafx.scene.control.MultipleSelectionModel;
-import org.apache.sis.image.ResampledImage;
-import org.apache.sis.internal.gui.Resources;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.internal.gui.GUIUtilities;
-
-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
- * @module
- */
-enum ImageOperation {
-    /**
-     * No operation applied.
-     */
-    NONE(Vocabulary.format(Vocabulary.Keys.None), 0),
-
-    /**
-     * Produces an image showing an estimation of positional error for each pixel.
-     */
-    POSITIONAL_ERROR(Resources.format(Resources.Keys.PositionalErrors), 2) {
-        @Override final boolean isEnabled(final RenderedImage source) {
-            return (source instanceof ResampledImage) && !((ResampledImage) source).isLinear();
-        }
-
-        @Override final RenderedImage apply(final RenderedImage source) {
-            final Object value = source.getProperty(POSITIONAL_ERRORS_KEY);
-            return (value instanceof RenderedImage) ? (RenderedImage) value : source;
-        }
-    };
-
-    /**
-     * The label to show in the list view.
-     */
-    private final String label;
-
-    /**
-     * Number of fraction digits to use when formatting sample values.
-     * Ignored in the special case of {@link #NONE}.
-     */
-    final int fractionDigits;
-
-    /**
-     * Creates a new operation.
-     */
-    private ImageOperation(final String label, final int fractionDigits) {
-        this.label = label;
-        this.fractionDigits = fractionDigits;
-    }
-
-    /**
-     * Updates the given list of operations with operations that are enabled for the given image.
-     * Operations that are not enabled are removed from the list.
-     *
-     * @param  list   the list to update.
-     * @param  image  the new image.
-     */
-    static void update(final ListView<ImageOperation> list, final RenderedImage image) {
-        final EnumSet<ImageOperation> updated = EnumSet.allOf(ImageOperation.class);
-        updated.removeIf((op) -> !op.isSourceEnabled(image));
-        final MultipleSelectionModel<ImageOperation> selection = list.getSelectionModel();
-        final boolean unselect = !updated.contains(selection.getSelectedItem());
-        GUIUtilities.copyAsDiff(Arrays.asList(updated.toArray(new ImageOperation[updated.size()])), list.getItems());
-        if (unselect) {
-            selection.select(0);
-        }
-    }
-
-    /**
-     * Returns whether this operation can be applied for the given image.
-     * The given image should be the same than the one given to {@link #apply(RenderedImage)}.
-     */
-    boolean isEnabled(final RenderedImage source) {
-        return true;
-    }
-
-    /**
-     * Returns whether this operation can be applied for the given image of a parent of this image.
-     */
-    private boolean isSourceEnabled(final RenderedImage source) {
-        if (source != null) {
-            if (isEnabled(source)) {
-                return true;
-            }
-            final List<RenderedImage> sources = source.getSources();
-            if (sources != null) {
-                for (final RenderedImage parent : sources) {
-                    if (isSourceEnabled(parent)) {
-                        return true;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Applies the operation on given image. If a map projection or a zoom has been applied, then the given
-     * image should be the resampled image (instead than the image read directly from the {@code DataStore}).
-     * If the operation can not be applied, then {@code source} is returned as-is.
-     */
-    RenderedImage apply(final RenderedImage source) {
-        return source;
-    }
-
-    /**
-     * Returns the label to show in menu.
-     */
-    @Override
-    public String toString() {
-        return label;
-    }
-}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
new file mode 100644
index 0000000..7484d9c
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
@@ -0,0 +1,822 @@
+/*
+ * 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.Date;
+import java.util.Locale;
+import java.util.List;
+import java.util.Map;
+import java.util.IdentityHashMap;
+import java.util.function.Predicate;
+import java.text.NumberFormat;
+import java.io.IOException;
+import java.awt.Rectangle;
+import java.awt.image.RenderedImage;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ObjectPropertyBase;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.TreeCell;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import javafx.util.Callback;
+import org.apache.sis.gui.Widget;
+import org.apache.sis.image.PlanarImage;
+import org.apache.sis.image.ResampledImage;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.gui.Styles;
+import org.apache.sis.internal.gui.PropertyView;
+import org.apache.sis.internal.gui.ImmutableObjectProperty;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.util.PropertyFormat;
+
+
+/**
+ * Information about {@link RenderedImage} (sources, layout, properties).
+ * The {@link #image} property value is shown as the root of a tree of images,
+ * with image {@linkplain RenderedImage#getSources() sources} as children.
+ * When an image is selected, its layout (image size, tile size, <i>etc.</i>) is described in a table.
+ * Image {@linkplain RenderedImage#getPropertyNames() properties} are also available in a separated table.
+ *
+ * <p>This widget is useful mostly for debugging purposes or for advanced users.
+ * For displaying a geospatial raster as a GIS application, see {@link CoverageCanvas} instead.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public class ImagePropertyExplorer extends Widget {
+    /**
+     * The root image to describe. This image will be the root of a tree;
+     * children will be image {@linkplain RenderedImage#getSources() sources}.
+     */
+    public final ObjectProperty<RenderedImage> image;
+
+    /**
+     * Implementation of {@link #image} property.
+     */
+    private final class ImageProperty extends ObjectPropertyBase<RenderedImage> {
+        /** Returns the bean that contains this property. */
+        @Override public Object getBean() {return ImagePropertyExplorer.this;}
+        @Override public String getName() {return "image";}
+
+        /** Sets this property to the given value with no sub-region. */
+        @Override public void set(RenderedImage newValue) {setImage(newValue, null);}
+
+        /** Do the actual set operation without invoking {@link ImagePropertyExplorer} setter method. */
+        void assign(RenderedImage newValue) {super.set(newValue);}
+    }
+
+    /**
+     * Image region which is currently visible, or {@code null} if unspecified.
+     * Conceptually this field and {@link #image} should be set together. But we have not defined
+     * a container object for those two properties. So we use that field as a workaround for now.
+     *
+     * @see #setImage(RenderedImage, Rectangle)
+     */
+    private Rectangle visibleImageBounds;
+
+    /**
+     * Whether {@link #visibleImageBounds} applies to the coordinate system of an image.
+     * This is initially {@code true} for an image specified by {@link CoverageCanvas} and become {@code false}
+     * after a {@link ResampledImage} is found. Images not present in this map are implicitly associated to the
+     * {@code false} value.
+     *
+     * <p>This map is also opportunistically used for avoiding never-ending recursivity
+     * during the traversal of image sources.</p>
+     *
+     * @see #setImage(RenderedImage, Rectangle)
+     */
+    private final Map<RenderedImage,Boolean> imageUseBoundsCS;
+
+    /**
+     * Whether to update {@code ImagePropertyExplorer} content when the {@link #image} changed.
+     * This is usually {@code true} unless this {@code ImagePropertyExplorer} is hidden,
+     * in which case it may be useful to temporary disable updates for saving CPU times.
+     *
+     * <div class="note"><b>Example:</b>
+     * if this {@code ImagePropertyExplorer} is shown in a {@link TitledPane}, one can bind this property
+     * to {@link TitledPane#expandedProperty()} for updating the content only if the pane is visible.
+     * </div>
+     *
+     * Note that setting this property to {@code false} may have the effect of discarding current content
+     * when the {@link #image} change. This is done for allowing the garbage collector to reclaim memory.
+     * The content is reset to {@link #image} properties when {@code updateOnChange} become {@code true} again.
+     */
+    public final BooleanProperty updateOnChange;
+
+    /**
+     * Whether to notify {@code ImagePropertyExplorer} about {@link #image} changes.
+     * This may become {@code false} after {@link #updateOnChange} (not in same time),
+     * and reset to {@code true} when {@code updateOnChange} become {@code true} again.
+     *
+     * @see #updateOnChange
+     * @see #setEnabled(boolean)
+     */
+    private boolean listening;
+
+    /**
+     * The root {@link #image} and its sources as a tree. The root value may be {@code null} and the children
+     * removed if the tree needs to be rebuilt after an {@linkplain #image} change and this rebuild has been
+     * deferred ({@link #updateOnChange} is {@code false}).
+     */
+    private final TreeItem<RenderedImage> sourcesRoot;
+
+    /**
+     * The selected item in the sources tree.
+     *
+     * @see #getSelectedImage()
+     */
+    private final ReadOnlyObjectProperty<TreeItem<RenderedImage>> selectedImage;
+
+    /**
+     * The rows in the table showing layout information (image size, tile size, image position, <i>etc</i>).
+     * This list should be considered read-only.
+     */
+    private final ObservableList<LayoutRow> layoutRows;
+
+    /**
+     * A row in the table showing image layout. The inherited {@link String} property is the label to show in
+     * the first column. That label never change, contrarily to the {@link #xp} and {@link #yp} property values
+     * which are updated every time that we need to update the content for a new image.
+     */
+    private static final class LayoutRow extends ImmutableObjectProperty<String> {
+        /**
+         * Row indices where {@link LayoutRow} instances are shown, when all rows are present.
+         * Rows {@link #DISPLAYED_SIZE} and {@link #MIN_VISIBLE} may be absent, in which case
+         * next rows have their position shifted.
+         */
+        static final int IMAGE_SIZE = 0, DISPLAYED_SIZE = 1, TILE_SIZE = 2, NUM_TILES = 3,
+                         MIN_PIXEL = 4, MIN_VISIBLE = 5, MIN_TILE = 6;
+
+        /**
+         * Creates all rows.
+         */
+        static LayoutRow[] values(final Vocabulary vocabulary, final Resources resources) {
+            final LayoutRow[] rows = new LayoutRow[7];
+            rows[IMAGE_SIZE]     = new LayoutRow(true,  vocabulary.getString(Vocabulary.Keys.ImageSize));
+            rows[DISPLAYED_SIZE] = new LayoutRow(false, resources .getString(Resources .Keys.DisplayedSize));
+            rows[TILE_SIZE]      = new LayoutRow(true,  vocabulary.getString(Vocabulary.Keys.TileSize));
+            rows[NUM_TILES]      = new LayoutRow(true,  vocabulary.getString(Vocabulary.Keys.NumberOfTiles));
+            rows[MIN_PIXEL]      = new LayoutRow(true,  resources .getString(Resources .Keys.ImageStart));
+            rows[MIN_VISIBLE]    = new LayoutRow(false, resources .getString(Resources .Keys.DisplayStart));
+            rows[MIN_TILE]       = new LayoutRow(true,  resources .getString(Resources .Keys.TileIndexStart));
+            return rows;
+        }
+
+        /** Size or position along x and y axes, to show in second and third columns. */
+        final IntegerProperty xp, yp;
+
+        /**
+         * Whether this property is a core property to keep always visible.
+         */
+        private final boolean core;
+
+        /** Creates a new row with the given label in first column. */
+        private LayoutRow(final boolean core, final String label) {
+            super(label);
+            this.core = core;
+            xp = new SimpleIntegerProperty();
+            yp = new SimpleIntegerProperty();
+        }
+
+        /**
+         * Updates {@link #xp} and {@link #yp} property values for the given image.
+         * The index <var>i</var> is the row index when no filtering is applied.
+         */
+        final void update(final RenderedImage image, final Rectangle visibleImageBounds, final int i) {
+            int x = 0, y = 0;
+            if (image != null) switch (i) {
+                case IMAGE_SIZE:   x = image.getWidth();     y = image.getHeight();     break;
+                case TILE_SIZE:    x = image.getTileWidth(); y = image.getTileHeight(); break;
+                case NUM_TILES:    x = image.getNumXTiles(); y = image.getNumYTiles();  break;
+                case MIN_TILE:     x = image.getMinTileX();  y = image.getMinTileY();   break;
+                case MIN_PIXEL:    x = image.getMinX();      y = image.getMinY();       break;
+                case MIN_VISIBLE:  if (visibleImageBounds != null) {
+                                       x = visibleImageBounds.x;
+                                       y = visibleImageBounds.y;
+                                   }
+                                   break;
+                case DISPLAYED_SIZE: if (visibleImageBounds != null) {
+                                       x = visibleImageBounds.width;
+                                       y = visibleImageBounds.height;
+                                   }
+                                   break;
+            }
+            xp.set(x);
+            yp.set(y);
+        }
+
+        /**
+         * Filter for excluding the rows that need a non-null {@code visibleImageBounds} argument.
+         */
+        static Predicate<LayoutRow> EXCLUDE_VISIBILITY = (r) -> r.core;
+    }
+
+    /**
+     * The predicate for filtering {@link #layoutRows}.
+     *
+     * @see LayoutRow#EXCLUDE_VISIBILITY
+     */
+    private final ObjectProperty<Predicate<? super LayoutRow>> layoutFilter;
+
+    /**
+     * The rows in the tables showing property values.
+     * Rows in the list will be added and removed when the image changed.
+     *
+     * @see #updatePropertyList(RenderedImage)
+     */
+    private final ObservableList<PropertyRow> propertyRows;
+
+    /**
+     * The selected item in the table of properties.
+     */
+    private final ReadOnlyObjectProperty<PropertyRow> selectedProperty;
+
+    /**
+     * A row in the table showing image properties. The inherited {@link String} property is the property name.
+     * The property value is fetched from the given image and can be updated for the value of a new image.
+     * Updating an existing {@code PropertyRow} instead than creating a new instance is useful for keeping
+     * the selected row unchanged if possible.
+     */
+    private static final class PropertyRow extends ImmutableObjectProperty<String> {
+        /**
+         * Image image property.
+         */
+        final ObjectProperty<Object> value;
+
+        /**
+         * Creates a new row for the given property in the given image.
+         */
+        PropertyRow(final RenderedImage image, final String property) {
+            super(property);
+            value = new SimpleObjectProperty<>(getProperty(image, property));
+        }
+
+        /**
+         * If this property can be updated to a value for the given image, performs
+         * the update and returns {@code true}. Otherwise returns {@code false}.
+         */
+        final boolean update(final RenderedImage image, final String property) {
+            if (property.equals(super.get())) {
+                value.set(getProperty(image, property));
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Returns a property value of given image, or the exception if that operation failed.
+         */
+        private static Object getProperty(final RenderedImage image, final String property) {
+            try {
+                return image.getProperty(property);
+            } catch (RuntimeException e) {
+                return e;
+            }
+        }
+
+        /**
+         * Returns a human-readable variation of the property name for use in graphic interface.
+         */
+        @Override
+        public String get() {
+            final String property = super.get();
+            return CharSequences.camelCaseToSentence(property.substring(property.lastIndexOf('.') + 1)).toString();
+        }
+    }
+
+    /**
+     * The tab where to show details about a property value. The content of tab may be different kinds
+     * of node depending on the class of the property to be show.
+     *
+     * @see #propertyDetails
+     * @see #updatePropertyDetails(Rectangle)
+     */
+    private final Tab detailsTab;
+
+    /**
+     * Viewer of property value. The different components of this viewer are created when first needed.
+     *
+     * @see #updatePropertyDetails(Rectangle)
+     */
+    private final PropertyView propertyDetails;
+
+    /**
+     * The view containing all visual components.
+     * The exact class may change in any future version.
+     */
+    private final SplitPane view;
+
+    /**
+     * Creates an initially empty explorer.
+     */
+    public ImagePropertyExplorer() {
+        this(null, null);
+    }
+
+    /**
+     * Creates a new explorer.
+     *
+     * @param  background  the image background color, or {@code null} if none.
+     */
+    ImagePropertyExplorer(final Locale locale,  final ObjectProperty<Background> background) {
+        final Vocabulary vocabulary = Vocabulary.getResources(locale);
+        final Resources  resources  = Resources.forLocale(locale);
+
+        // Following variables could be class fields, but are not yet needed outside this constructor.
+        final TreeView<RenderedImage> sources;
+        final TableView<LayoutRow>    layout;
+        final NumberFormat            integerFormat;
+        final TableView<PropertyRow>  properties;
+        final TabPane                 tabPane;
+
+        image            = new ImageProperty();
+        imageUseBoundsCS = new IdentityHashMap<>(4);
+        updateOnChange   = new SimpleBooleanProperty(this, "updateOnChange", true);
+        listening        = true;
+        /*
+         * Tree of image sources. The root is never changed after construction. All children nodes can
+         * be created, removed or updated to new value at any time. At most one image can be selected.
+         */
+        {
+            sourcesRoot   = new TreeItem<>();
+            sources       = new TreeView<>(sourcesRoot);
+            selectedImage = sources.getSelectionModel().selectedItemProperty();
+            sources.setCellFactory(SourceCell::new);
+            selectedImage.addListener((p,o,n) -> {
+                final RenderedImage selected = n.getValue();
+                imageSelected(selected != null ? selected : image.get());
+            });
+        }
+        /*
+         * Table of image layout built with a fixed set of rows: no row will be added or removed after
+         * construction. Instead property values of existing rows will be modified when a new image is
+         * selected. Row selection are not allowed since we have nothing to do with selected rows.
+         */
+        {
+            final FilteredList<LayoutRow> filtered;
+            layoutRows     = FXCollections.observableArrayList(LayoutRow.values(vocabulary, resources));
+            filtered       = new FilteredList<>(layoutRows);
+            layout         = new TableView<>(filtered);
+            layoutFilter   = filtered.predicateProperty();
+            integerFormat  = NumberFormat.getIntegerInstance();
+            layout.setSelectionModel(null);
+
+            final TableColumn<LayoutRow, String> label = new TableColumn<>(resources.getString(Resources.Keys.SizeOrPosition));
+            final TableColumn<LayoutRow, Number> xCol  = new TableColumn<>(resources.getString(Resources.Keys.Along_1, "X"));
+            final TableColumn<LayoutRow, Number> yCol  = new TableColumn<>(resources.getString(Resources.Keys.Along_1, "Y"));
+            final Callback<TableColumn<LayoutRow, Number>,
+                             TableCell<LayoutRow, Number>> cellFactory = (column) -> new LayoutCell(integerFormat);
+
+            xCol .setCellFactory(cellFactory);
+            yCol .setCellFactory(cellFactory);
+            xCol .setCellValueFactory((cell) -> cell.getValue().xp);
+            yCol .setCellValueFactory((cell) -> cell.getValue().yp);
+            label.setCellValueFactory((cell) -> cell.getValue());
+            layout.getColumns().setAll(label, xCol, yCol);
+            layout.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+            layout.getColumns().forEach((c) -> {
+                c.setReorderable(false);
+                c.setSortable(false);
+            });
+        }
+        /*
+         * Table of image properties. Contrarily to the layout table, the set of rows in
+         * this property table may change at any time. At most one row can be selected.
+         * We do not register a listener on the row selection; instead we wait for the
+         * details pane to become visible.
+         */
+        {
+            properties       = new TableView<>();
+            propertyRows     = properties.getItems();
+            selectedProperty = properties.getSelectionModel().selectedItemProperty();
+            final TableColumn<PropertyRow, String> label = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Property));
+            final TableColumn<PropertyRow, Object> value = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Value));
+            label.setCellValueFactory((cell) -> cell.getValue());
+            value.setCellValueFactory((cell) -> cell.getValue().value);
+            value.setCellFactory((column) -> new PropertyCell(locale));
+            properties.getColumns().setAll(label, value);
+            properties.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+            properties.getColumns().forEach((c) -> c.setReorderable(false));
+        }
+        /*
+         * Tab where to show details about the currently selected property value.
+         * The tab content is updated when it become visible. We can do that because
+         * the property selection is done in another tab.
+         */
+        {
+            detailsTab = new Tab(vocabulary.getString(Vocabulary.Keys.Details));
+            selectedProperty.addListener((p,o,n) -> clearPropertyValues(false));
+            propertyDetails = new PropertyView(locale, detailsTab.contentProperty(), background);
+            detailsTab.selectedProperty().addListener((p,o,n) -> {
+                if (n) updatePropertyDetails(getVisibleImageBounds(getSelectedImage()));
+            });
+        }
+        /*
+         * The view containing all visual components. A minimal height is given to `sources` tree
+         * because otherwise it appears with a height of 0 every time the `TitledPane` is expanded.
+         */
+        tabPane = new TabPane(
+                new Tab(vocabulary.getString(Vocabulary.Keys.Layout), layout),
+                new Tab(vocabulary.getString(Vocabulary.Keys.Properties), properties),
+                detailsTab);
+        tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
+        view = new SplitPane(sources, tabPane);
+        view.setOrientation(Orientation.VERTICAL);
+        SplitPane.setResizableWithParent(sources, false);
+        sources.setMinHeight(50);
+        updateOnChange.addListener((p,o,n) -> {setEnabled(n);});
+    }
+
+    /**
+     * Invoked when {@link #updateOnChange} became {@code true}.
+     * This method updates the visual components for current image.
+     */
+    private void setEnabled(final boolean enabled) {
+        if (enabled) {
+            listening = true;
+            if (sourcesRoot.getValue() == null) {
+                setTreeRoot(image.get());
+                refreshTables();
+            }
+        } else {
+            // Do not set `listening = false` now; it may be done by `setImage(…)` later.
+        }
+    }
+
+    /**
+     * Sets the image to show together with the coordinates of the region currently shown.
+     * If {@link #updateOnChange} is true, then the tree view is updated.
+     * Otherwise we will wait for the tree view to become visible before to update it.
+     *
+     * @param newValue       the new image.
+     * @param visibleBounds  image region which is currently visible, or {@code null} if unspecified.
+     */
+    final void setImage(final RenderedImage newValue, final Rectangle visibleBounds) {
+        visibleImageBounds = visibleBounds;
+        ((ImageProperty) image).assign(newValue);
+        if (listening) {
+            final boolean immediate = updateOnChange.get();
+            setTreeRoot(immediate ? newValue : null);
+            if (immediate) {
+                refreshTables();
+            } else {
+                clearPropertyValues(true);
+                listening = false;
+            }
+        }
+    }
+
+    /**
+     * Returns the currently selected image. If no image is explicitly selected,
+     * returns the root {@linkplain #image} (which may be null).
+     */
+    private RenderedImage getSelectedImage() {
+        final TreeItem<RenderedImage> item = selectedImage.get();
+        if (item != null) {
+            final RenderedImage selected = item.getValue();
+            if (selected != null) return selected;
+        }
+        return image.get();
+    }
+
+    /**
+     * Refresh all visual components except the tree of sources. This includes the table of
+     * image layout, the table of property values and the details of selected property value.
+     */
+    private void refreshTables() {
+        imageSelected(getSelectedImage());
+    }
+
+    /**
+     * Invoked when an image is selected in the tree of image sources. The selected image
+     * is not necessarily {@link #image} property value; it may be one if its sources.
+     * If no image is explicitly selected, defaults to the root image.
+     */
+    private void imageSelected(final RenderedImage selected) {
+        final Rectangle bounds = getVisibleImageBounds(selected);
+        final int n = layoutRows.size();
+        for (int i=0; i<n; i++) {
+            layoutRows.get(i).update(selected, bounds, i);
+        }
+        layoutFilter.set(bounds != null ? null : LayoutRow.EXCLUDE_VISIBILITY);
+        updatePropertyList(selected);
+        /*
+         * The selected property value may have changed as a result of above.
+         * If the details tab is visible, update immediately. Otherwise we will
+         * wait for that tab to become visible.
+         */
+        if (detailsTab.isSelected()) {
+            updatePropertyDetails(bounds);
+        }
+    }
+
+    /**
+     * Returns the pixel coordinates of the region shown on screen,
+     * or {@code null} if none or does not apply to the currently selected image.
+     */
+    private Rectangle getVisibleImageBounds(final RenderedImage selected) {
+        return Boolean.TRUE.equals(imageUseBoundsCS.get(selected)) ? visibleImageBounds : null;
+    }
+
+    /**
+     * Sets the root image together with its tree of sources.
+     */
+    private void setTreeRoot(final RenderedImage newValue) {
+        imageUseBoundsCS.clear();
+        setTreeNode(sourcesRoot, newValue, imageUseBoundsCS, visibleImageBounds != null);
+        /*
+         * Remove entries associated to value `false` since our default value is `false`.
+         * The intent is to avoid unnecessary `RenderedImage` references for reducing the
+         * risk of memory retention.
+         */
+        imageUseBoundsCS.values().removeIf((b) -> !b);
+    }
+
+    /**
+     * Invoked when tree under {@link #sourcesRoot} node needs to be updated. This method is not necessarily invoked
+     * immediately after an {@linkplain #image} change; the update may be deferred until the tree become visible.
+     *
+     * @param  imageUseBoundsCS  the {@link #imageUseBoundsCS} as an initially empty map. This map is
+     *         populated by this method and opportunistically used for avoiding infinite recursivity.
+     */
+    private static void setTreeNode(final TreeItem<RenderedImage> root, final RenderedImage image,
+            final Map<RenderedImage,Boolean> imageUseBoundsCS, Boolean boundsApplicable)
+    {
+        root.setValue(image);
+        if (imageUseBoundsCS.putIfAbsent(image, boundsApplicable) == null) {
+            final ObservableList<TreeItem<RenderedImage>> children = root.getChildren();
+            if (image != null) {
+                final List<RenderedImage> sources = image.getSources();
+                if (sources != null) {
+                    /*
+                     * If the image is an instance of `ResampledImage`, then its
+                     * source is presumed to use a different coordinate system.
+                     */
+                    if (image instanceof ResampledImage) {
+                        boundsApplicable = Boolean.FALSE;
+                    }
+                    final int numSrc = sources.size();
+                    final int numDst = children.size();
+                    final int n = Math.min(numSrc, numDst);
+                    int i;
+                    for (i=0; i<n; i++) {
+                        setTreeNode(children.get(i), sources.get(i), imageUseBoundsCS, boundsApplicable);
+                    }
+                    for (; i<numSrc; i++) {
+                        final TreeItem<RenderedImage> child = new TreeItem<>();
+                        setTreeNode(child, sources.get(i), imageUseBoundsCS, boundsApplicable);
+                        children.add(child);
+                    }
+                    if (i < numDst) {
+                        children.remove(i, numDst);
+                    }
+                    return;
+                }
+            }
+            children.clear();
+        }
+    }
+
+    /**
+     * Creates the renderer of cells in the tree of image sources.
+     */
+    private static final class SourceCell extends TreeCell<RenderedImage> {
+        /**
+         * Invoked by the cell factory (must have this exact signature).
+         */
+        SourceCell(final TreeView<RenderedImage> tree) {
+        }
+
+        /**
+         * Invoked when a new image is shown in this cell node. This method also tests image consistency.
+         * If an inconsistency is found, the line is shown in read with a warning message.
+         */
+        @Override protected void updateItem(final RenderedImage image, final boolean empty) {
+            super.updateItem(image, empty);
+            String text = null;
+            Color  fill = Styles.NORMAL_TEXT;
+            if (image != null) {
+                text = getClassName(image.getClass());
+                if (image instanceof PlanarImage) {
+                    final String check = ((PlanarImage) image).verify();
+                    if (check != null) {
+                        text = Resources.format(Resources.Keys.InconsistencyIn_2, text, check);
+                        fill = Styles.ERROR_TEXT;
+                    }
+                }
+            }
+            setText(text);
+            setTextFill(fill);
+        }
+    }
+
+    /**
+     * Gets a simple top-level class name for an image class. If the given type is an enclosed class,
+     * searches for a parent class instead because enclosed class names are often not very informative.
+     * For example {@code ImageRenderer.Untitled} which is a {@code BufferedImage} subclass:
+     * the enclosing class name is not suitable in that example.
+     */
+    private static String getClassName(Class<?> type) {
+        while (type.getEnclosingClass() != null) {
+            type = type.getSuperclass();
+        }
+        return type.getSimpleName();
+    }
+
+    /**
+     * Creates the renderer of cells in the table of image layout information.
+     */
+    private static final class LayoutCell extends TableCell<LayoutRow,Number> {
+        /**
+         * The formatter to use for numerical values in the table.
+         */
+        private final NumberFormat integerFormat;
+
+        /**
+         * Invoked by the cell factory.
+         */
+        LayoutCell(final NumberFormat integerFormat) {
+            this.integerFormat = integerFormat;
+            setAlignment(Pos.CENTER_RIGHT);
+        }
+
+        /**
+         * Invoked when a new value is shown in this table cell.
+         */
+        @Override protected void updateItem(final Number value, final boolean empty) {
+            super.updateItem(value, empty);
+            setText(value != null ? integerFormat.format(value) : null);
+        }
+    }
+
+    /**
+     * Creates the renderer of cells in the table of image properties.
+     */
+    private static final class PropertyCell extends TableCell<PropertyRow,Object> {
+        /**
+         * The formatter to use for producing a short string representation of a property value.
+         */
+        private final ValueFormat format;
+
+        /** {@link PropertyCell#format} implementation. */
+        private static final class ValueFormat extends PropertyFormat {
+            /** The locale to use for objects such as international strings. */
+            private final Locale locale;
+
+            /** Creates a new formatter which will write in the given buffer. */
+            ValueFormat(final Locale locale, final StringBuilder buffer) {
+                super(buffer);
+                this.locale = locale;
+            }
+
+            /** Returns the locale specified at construction time. */
+            @Override public Locale getLocale() {return locale;}
+
+            /** Invoked by {@link PropertyFormat} for values of unrecognized type. */
+            @Override protected String toString(final Object value) {
+                if (value instanceof Number || value instanceof Date) {     // See super-class javadoc.
+                    return value.toString();
+                }
+                return getClassName(value.getClass()) + "[…]";
+            }
+        }
+
+        /**
+         * Temporary buffer user when formatting property values.
+         */
+        private final StringBuilder buffer;
+
+        /**
+         * Invoked by the cell factory.
+         */
+        PropertyCell(final Locale locale) {
+            buffer = new StringBuilder();
+            format = new ValueFormat(locale, buffer);
+        }
+
+        /**
+         * Invoked when a new value is shown in this table cell.
+         */
+        @Override protected void updateItem(final Object value, final boolean empty) {
+            super.updateItem(value, empty);
+            String text = null;
+            if (!empty) try {
+                buffer.setLength(0);
+                format.appendValue(value);
+                format.flush();
+                text = buffer.toString();
+            } catch (IOException e) {           // Should never happen since we write in a StringBuilder.
+                text = e.toString();
+            }
+            setText(text);
+        }
+    }
+
+    /**
+     * Update the list of properties for the given image.
+     * The {@link #propertyRows} are updated with an effort for reusing existing items when
+     * the property name is the same. The intent is to keep selection unchanged if possible
+     * (because removing a selected row may make it unselected).
+     */
+    private void updatePropertyList(final RenderedImage selected) {
+        if (selected != null) {
+            final String[] properties = selected.getPropertyNames();
+            if (properties != null) {
+                int insertAt = 0;
+nextProp:       for (final String property : properties) {
+                    if (property != null) {
+                        for (int i=insertAt; i < propertyRows.size(); i++) {
+                            if (propertyRows.get(i).update(selected, property)) {
+                                propertyRows.remove(insertAt, i);
+                                insertAt = i + 1;
+                                continue nextProp;
+                            }
+                        }
+                        propertyRows.add(insertAt++, new PropertyRow(selected, property));
+                    }
+                }
+                propertyRows.remove(insertAt, propertyRows.size());
+                return;
+            }
+        }
+        propertyRows.clear();
+    }
+
+    /**
+     * Updates the {@link #detailsTab} with the value of currently selected property.
+     * This method may be invoked after the selection changed (but not immediately),
+     * or after the selected image changed (which indirectly changes the properties).
+     *
+     * @param  bounds  {@link #visibleImageBounds} or {@code null} if it does not apply to current image.
+     */
+    private void updatePropertyDetails(final Rectangle bounds) {
+        final PropertyRow row = selectedProperty.get();
+        propertyDetails.set((row != null) ? row.value.get() : null, bounds);
+    }
+
+    /**
+     * Clears the table of property values and the content of {@link #detailsTab}.
+     * We do that when the tab became hidden and the image changed, in order to give
+     * a chance to the garbage collector to release memory.
+     *
+     * @param  full  whether to clears also the table in the "properties" tab (in addition of clearing the
+     *         "details" tab). This parameter should be {@code false} if the properties tab is still visible.
+     */
+    private void clearPropertyValues(final boolean full) {
+        if (propertyDetails != null) {
+            propertyDetails.clear();
+            detailsTab.setContent(null);
+        }
+        if (full) {
+            propertyRows.clear();
+        }
+    }
+
+    /**
+     * Returns the view of this explorer. The subclass is implementation dependent
+     * and may change in any future version.
+     *
+     * @return this explorer view.
+     */
+    @Override
+    public Region getView() {
+        return view;
+    }
+}
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 a88ae5b..a423f6a 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
@@ -129,7 +129,7 @@ final class RenderingData implements Cloneable {
     /**
      * Key of the currently selected alternative in {@link CoverageCanvas#resampledImages} map.
      */
-    ImageDerivative selectedDerivative;
+    Stretching selectedDerivative;
 
     /**
      * Statistics on pixel values of current {@link #data}, or {@code null} if none or not yet computed.
@@ -148,7 +148,7 @@ final class RenderingData implements Cloneable {
      * @todo Listen to logging messages. We need to create a logging panel first.
      */
     RenderingData() {
-        selectedDerivative = ImageDerivative.NONE;
+        selectedDerivative = Stretching.NONE;
         processor = new ImageProcessor();
         processor.setErrorAction(ImageProcessor.ErrorAction.LOG);
     }
@@ -273,41 +273,18 @@ 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, final Rectangle2D displayBounds) {
-        /*
-         * If the operation is not `NONE` but following call to `apply(…)` returns `resampledImage` unchanged,
-         * it means that the operation can not be applied on that image. We should reset operation to `NONE`,
-         * update UI by disabling operation and keep `StatusBarSupport.selectedProvider` to its default value.
-         * For now we avoid that complexity since we need to define a better coverage operation framework anyway.
-         */
-        resampledImage = selectedDerivative.operation.apply(resampledImage);
-        if (selectedDerivative.styling != Stretching.NONE) {
+        if (selectedDerivative != Stretching.NONE) {
             final Map<String,Object> modifiers = new HashMap<>(4);
             /*
-             * If no operation is applied, select the original image as the source of statistics.
-             * It saves computation time (no need to recompute the statistics when the projection
-             * is changed) and provides more stable visual output (color ramp computed from same
-             * standard deviation in "automatic" mode). If an operation is applied, the resulting
-             * image can be anything so we let `stretchColorRamp(…)` uses statistics on that image.
+             * Select the original image as the source of statistics. It saves computation time (no need
+             * to recompute the statistics when the projection is changed) and provides more stable visual
+             * output (color ramp computed from same standard deviation in "automatic" mode).
              */
-            if (selectedDerivative.operation == ImageOperation.NONE) {
-                if (statistics == null) {
-                    statistics = processor.getStatistics(data, null);
-                }
-                modifiers.put("statistics", statistics);
-            } else {
-                /*
-                 * If an operation is applied, compute statistics only for currently visible region.
-                 * This is necessary because zoomed images may be very large. This is usually not a
-                 * problem because only requested tiles are computed, but statistics requested without
-                 * bounds would cause all those tiles to be computed.
-                 *
-                 * Inconvenient is that visual appareance is not stable: the color ramp may change
-                 * after every zoom, or may not fit data anymore after a pan. Since we need to revisit
-                 * the coverage operation framework anyway, we live with that problem for now.
-                 */
-                modifiers.put("areaOfInterest", displayBounds.getBounds());
+            if (statistics == null) {
+                statistics = processor.getStatistics(data, null);
             }
-            if (selectedDerivative.styling == Stretching.AUTOMATIC) {
+            modifiers.put("statistics", statistics);
+            if (selectedDerivative == Stretching.AUTOMATIC) {
                 modifiers.put("MultStdDev", 3);
             }
             return processor.stretchColorRamp(resampledImage, modifiers);
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
deleted file mode 100644
index 54a426b..0000000
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StatusBarSupport.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * 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 javafx.scene.control.ListView;
-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.
- *
- * <p>The fields in this class could have been declared directly into {@link CoverageCanvas}.
- * But we keep this class separated because it is a workaround for the lack of good public API
- * for describing coverage operations. We may remove this class in a future Apache SIS version
- * if SIS provides a more complete coverage operation framework.</p>
- *
- * @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;
-
-    /**
-     * The list of operations that can be applied on the image. Items may be added or removed in
-     * this list depending on whether the operation is possible for current {@linkplain #image}.
-     */
-    private final ListView<ImageOperation> operations;
-
-    /**
-     * The image from which sample values will be read, or {@code null} if none. This reference is declared here
-     * instead than in {@link #sampleValuesProviders} for having a single reference to update when image changes.
-     * This is desired for avoiding memory leaks.
-     */
-    private RenderedImage image;
-
-    /**
-     * Creates a new support class for the given status bar.
-     */
-    StatusBarSupport(final StatusBar bar, final ListView<ImageOperation> operations) {
-        this.operations = operations;
-        selectedProvider = bar.sampleValuesProvider;
-        sampleValuesProviders = new EnumMap<>(ImageOperation.class);
-        sampleValuesProviders.put(ImageOperation.NONE, selectedProvider.get());
-    }
-
-    /**
-     * Invoked after each rendering event for updating {@link StatusBar#sampleValuesProvider}
-     * with an instance appropriate for the image shown.
-     *
-     * @param  operation  the operation applied on the image.
-     * @param  data       the image resulting from the operation.
-     */
-    final void notifyImageShown(final ImageOperation operation, final RenderedImage data) {
-        image = data;
-        selectedProvider.set(sampleValuesProviders.computeIfAbsent(operation, (o) -> new Evaluator(o.fractionDigits)));
-        ImageOperation.update(operations, data);
-    }
-
-    /**
-     * Provides sample values for result of image operation.
-     * Current implementation assumes a single-banded image with a fixed number of fraction digits.
-     */
-    private final class Evaluator extends ValuesUnderCursor {
-        /**
-         * 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  fractionDigits  number of fraction digits.
-         */
-        Evaluator(final int fractionDigits) {
-            buffer = new StringBuffer();
-            pos    = new FieldPosition(0);
-            format = NumberFormat.getNumberInstance();
-            format.setMinimumFractionDigits(fractionDigits);
-            format.setMaximumFractionDigits(fractionDigits);
-            /*
-             * 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) {
-            final RenderedImage data = image;
-            if (data != null) 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/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index 5166416..9ce058b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -38,7 +38,6 @@ import javafx.beans.property.ReadOnlyBooleanWrapper;
 import javafx.beans.property.ReadOnlyObjectProperty;
 import javafx.beans.property.ReadOnlyObjectWrapper;
 import javafx.concurrent.Task;
-import javafx.scene.shape.Rectangle;
 import javafx.scene.transform.Affine;
 import javafx.scene.transform.NonInvertibleTransformException;
 import org.opengis.geometry.Envelope;
@@ -61,6 +60,7 @@ import org.apache.sis.util.logging.Logging;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.GUIUtilities;
 import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.portrayal.PlanarCanvas;
 import org.apache.sis.portrayal.RenderException;
@@ -291,10 +291,7 @@ public abstract class MapCanvas extends PlanarCanvas {
         view.setCursor(Cursor.CROSSHAIR);
         floatingPane = view;
         fixedPane = new StackPane(view);
-        final Rectangle clip = new Rectangle();
-        clip.widthProperty() .bind(fixedPane.widthProperty());
-        clip.heightProperty().bind(fixedPane.heightProperty());
-        fixedPane.setClip(clip);
+        GUIUtilities.setClipToBounds(fixedPane);
         isRendering = new ReadOnlyBooleanWrapper(this, "isRendering");
         error = new ReadOnlyObjectWrapper<>(this, "exception");
     }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
index 4db10f3..21c4866 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
@@ -26,6 +26,8 @@ import javafx.scene.Node;
 import javafx.scene.Scene;
 import javafx.scene.control.ContextMenu;
 import javafx.scene.control.MenuItem;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.layout.Pane;
 import javafx.stage.Window;
 import org.apache.sis.util.Static;
 
@@ -74,6 +76,20 @@ public final class GUIUtilities extends Static {
     }
 
     /**
+     * Sets on the given pane a clip defined to the pane bounds. This method is invoked for pane having content
+     * that may be drawn outside the pane bounds (typically images). We use this method as a workaround for the
+     * fact that JavaFX pane does not apply clip by itself.
+     *
+     * @param  pane  the pane on which to set the clip.
+     */
+    public static void setClipToBounds(final Pane pane) {
+        final Rectangle clip = new Rectangle();
+        clip.widthProperty() .bind(pane.widthProperty());
+        clip.heightProperty().bind(pane.heightProperty());
+        pane.setClip(clip);
+    }
+
+    /**
      * Copies all elements from the given source list to the specified target list,
      * but with the application of insertion and removal operations only.
      * This method is useful when the two lists should be similar.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
new file mode 100644
index 0000000..69fe555
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
@@ -0,0 +1,156 @@
+/*
+ * 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.internal.gui;
+
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.RenderedImage;
+import javafx.concurrent.Task;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.PixelWriter;
+import javafx.scene.image.WritableImage;
+import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.jdk9.JDK9;
+import org.apache.sis.math.Statistics;
+
+
+/**
+ * Converts a Java2D image to a JavaFX image, then writes the result in a given {@link ImageView}.
+ * This task should be used only for small images (thumbnail) because some potentially costly resources
+ * are created each time.
+ *
+ * <p>Current implementation returns statistics on sample values as a side-product.
+ * It may change in any future version.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class ImageConverter extends Task<Statistics[]> {
+    /**
+     * The maximal image width and height.
+     * This arbitrary value may change in any future version.
+     */
+    private static final int MAX_SIZE = 400;
+
+    /**
+     * The Java2D image to convert.
+     */
+    private final RenderedImage source;
+
+    /**
+     * Pixel coordinates of the region to render, or {@code null} for the whole image.
+     */
+    private Rectangle bounds;
+
+    /**
+     * Size of the image actually rendered. This is the {@link #bounds} size,
+     * unless that size is greater than {@link #MAX_SIZE} in which case it is scaled down.
+     */
+    private int width, height;
+
+    /**
+     * Where to write the image. This will be updated in JavaFX thread.
+     */
+    private final ImageView canvas;
+
+    /**
+     * The ARGB values to be copied in the JavaFX image.
+     */
+    private int[] data;
+
+    /**
+     * Creates a new task for converting the given image.
+     */
+    ImageConverter(final RenderedImage source, final Rectangle bounds, final ImageView canvas) {
+        this.source = source;
+        this.bounds = bounds;
+        this.canvas = canvas;
+    }
+
+    /**
+     * Prepares the ARGB values to be written in the JavaFX image.
+     */
+    @Override
+    protected Statistics[] call() {
+        if (bounds == null) {
+            bounds = ImageUtilities.getBounds(source);
+        }
+        width  = Math.min(bounds.width,  MAX_SIZE);
+        height = Math.min(bounds.height, MAX_SIZE);
+        final double scale = Math.max(width  / (double) bounds.width,
+                                      height / (double) bounds.height);
+        /*
+         * Use a uniform scale. At least one of `width` or `height` will be unchanged.
+         * The image will be shown fully, at the cost of some space being unused.
+         */
+        width  = (int) Math.round(scale * bounds.width);
+        height = (int) Math.round(scale * bounds.height);
+        final AffineTransform toCanvas = AffineTransform.getScaleInstance(scale, scale);
+        toCanvas.translate(-bounds.x, -bounds.y);
+
+        final ImageProcessor processor  = new ImageProcessor();
+        final Statistics[]   statistics = processor.getStatistics(source, bounds);
+        final RenderedImage  image      = processor.stretchColorRamp(source, JDK9.mapOf("MultStdDev", 3, "statistics", statistics));
+        final BufferedImage  buffer     = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
+        final Graphics2D     graphics   = buffer.createGraphics();
+        try {
+            graphics.drawRenderedImage(image, toCanvas);
+        } finally {
+            graphics.dispose();
+        }
+        data = ((DataBufferInt) buffer.getRaster().getDataBuffer()).getData();
+        return statistics;
+    }
+
+    /**
+     * Sets the JavaFX image to the ARGB values computed in background thread.
+     */
+    @Override
+    protected void succeeded() {
+        WritableImage destination = (WritableImage) canvas.getImage();
+        if (destination == null || destination.getWidth() != width || destination.getHeight() != height) {
+            destination = new WritableImage(width, height);
+        }
+        final PixelWriter writer = destination.getPixelWriter();
+        writer.setPixels(0, 0, width, height, PixelFormat.getIntArgbPreInstance(), data, 0, width);
+        data = null;
+        canvas.setImage(destination);
+    }
+
+    /**
+     * Discards ARGB values on failure.
+     */
+    @Override
+    protected void failed() {
+        data = null;
+    }
+
+    /**
+     * Discards ARGB values on cancellation.
+     */
+    @Override
+    protected void cancelled() {
+        data = null;
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
new file mode 100644
index 0000000..c933c5e
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
@@ -0,0 +1,112 @@
+/*
+ * 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.internal.gui;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.property.ReadOnlyObjectProperty;
+
+
+/**
+ * A property for a value that never change.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public class ImmutableObjectProperty<T> extends ReadOnlyObjectProperty<T> {
+    /**
+     * The object value.
+     */
+    private final T value;
+
+    /**
+     * Creates a new property for the given value.
+     *
+     * @param  value  the value.
+     */
+    public ImmutableObjectProperty(final T value) {
+        this.value = value;
+    }
+
+    /**
+     * Default to {@code null}.
+     *
+     * @return the object containing this property, or {@code null} if unspecified.
+     */
+    @Override
+    public Object getBean() {
+        return null;
+    }
+
+    /**
+     * Default to {@code null}.
+     *
+     * @return the property name, or {@code null} if unspecified.
+     */
+    @Override
+    public String getName() {
+        return null;
+    }
+
+    /**
+     * Returns the value specified at construction time.
+     *
+     * @return the property value.
+     */
+    @Override
+    public T get() {
+        return value;
+    }
+
+    /**
+     * Does nothing because the given listener would never be notified.
+     *
+     * @param  listener  ignored.
+     */
+    @Override
+    public void addListener(InvalidationListener listener) {
+    }
+
+    /**
+     * Does nothing because the given listener would never be notified.
+     *
+     * @param  listener  ignored.
+     */
+    @Override
+    public void addListener(ChangeListener<? super T> listener) {
+    }
+
+    /**
+     * Does nothing because no listener is registered.
+     *
+     * @param  listener  ignored.
+     */
+    @Override
+    public void removeListener(InvalidationListener listener) {
+    }
+
+    /**
+     * Does nothing because no listener is registered.
+     *
+     * @param  listener  ignored.
+     */
+    @Override
+    public void removeListener(ChangeListener<? super T> listener) {
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
new file mode 100644
index 0000000..50c719b
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
@@ -0,0 +1,300 @@
+/*
+ * 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.internal.gui;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.Format;
+import java.text.FieldPosition;
+import java.text.ParsePosition;
+import java.text.ParseException;
+import java.awt.Rectangle;
+import java.awt.image.RenderedImage;
+import javafx.beans.property.ObjectProperty;
+import javafx.concurrent.Task;
+import javafx.scene.Node;
+import javafx.scene.text.Font;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import org.apache.sis.io.CompoundFormat;
+import org.apache.sis.math.Statistics;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * A viewer for property value. The property may be of various class (array, image, <i>etc</i>).
+ * If the type is unrecognized, the property is shown as text.
+ *
+ * <p>This class extends {@link CompoundFormat} for implementation convenience only.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+@SuppressWarnings("serial")     // Not intended to be serialized.
+public final class PropertyView extends CompoundFormat<Object> {
+    /**
+     * The current property value. This is used for detecting changes.
+     */
+    private Object value;
+
+    /**
+     * The node used for showing {@link #value}.
+     */
+    private final ObjectProperty<Node> view;
+
+    /**
+     * Shows the {@linkplain #value} as plain text.
+     * This is built only when first needed.
+     */
+    private TextArea textView;
+
+    /**
+     * Shows the {@linkplain #value} as an image.
+     * This is built only when first needed.
+     */
+    private ImageView imageView;
+
+    /**
+     * The pane containing {@link #imageView}. We use that pane for allowing a background color to be specified.
+     * A future version may also use that pane for putting more visual components on top or below the image.
+     */
+    private final Pane imageCanvas;
+
+    /**
+     * The group of all components related to image, created when first needed.
+     * This includes {@link #imageView} and {@link #sampleValueRange}.
+     */
+    private GridPane imagePane;
+
+    /**
+     * Minimum and maximum values found in the image wrapped by {@link #imageView}.
+     * Created when first needed.
+     */
+    private Label sampleValueRange;
+
+    /**
+     * If the property is an image, bounds of currently visible region. May be {@code null} is unknown,
+     * in which case the whole image bounds is taken.
+     */
+    private Rectangle visibleImageBounds;
+
+    /**
+     * If a work is under progress, that work. Otherwise {@code null}.
+     */
+    private Task<?> runningTask;
+
+    /**
+     * Creates a new property view.
+     *
+     * @param  locale      the locale for numbers formatting.
+     * @param  view        the property where to set the node showing the value.
+     * @param  background  the image background color, or {@code null} if none.
+     */
+    public PropertyView(final Locale locale, final ObjectProperty<Node> view, final ObjectProperty<Background> background) {
+        super(locale, null);
+        this.view = view;
+        imageCanvas = new Pane();
+        if (background != null) {
+            imageCanvas.backgroundProperty().bind(background);
+        }
+    }
+
+    /**
+     * Required by {@link CompoundFormat} but not used.
+     *
+     * @return the base type of values formatted by this {@code PropertyView} instance.
+     */
+    @Override
+    public Class<? extends Object> getValueType() {
+        return Object.class;
+    }
+
+    /**
+     * Unsupported operation.
+     *
+     * @param  text ignored.
+     * @param  pos  ignored.
+     * @return never return.
+     * @throws ParseException always thrown.
+     */
+    @Override
+    public Object parse(CharSequence text, ParsePosition pos) throws ParseException {
+        throw new ParseException(null, 0);
+    }
+
+    /**
+     * Formats the given property value. Current implementation requires {@code toAppendTo}
+     * to be an instance of {@link StringBuffer}. This method is not intended to be invoked
+     * outside internal usage.
+     *
+     * @param  value       the property value to format.
+     * @param  toAppendTo  where to append the property value.
+     */
+    @Override
+    public void format(final Object value, final Appendable toAppendTo) throws IOException {
+        final Format f = getFormat(value.getClass());
+        if (f != null) {
+            f.format(value, (StringBuffer) toAppendTo, new FieldPosition(0));
+        } else {
+            toAppendTo.append(value.toString());
+        }
+    }
+
+    /**
+     * Formats a single value. This method does the same work than the inherited
+     * {@link #format(Object)} final method but in a more efficient way.
+     */
+    private String formatValue(final Object value) {
+        final Format f = getFormat(value.getClass());
+        if (f == null) {
+            return value.toString();
+        } else if (value instanceof Number) {
+            return Numerics.useScientificNotationIfNeeded(f, value, Format::format);
+        } else {
+            return f.format(value);
+        }
+    }
+
+    /**
+     * Formats the given value, using scientific notation if needed.
+     */
+    private static void format(final Format f, final double value, final StringBuffer buffer, final FieldPosition pos) {
+        Numerics.useScientificNotationIfNeeded(f, value, (nf,v) -> {nf.format(v, buffer, pos); return null;});
+    }
+
+    /**
+     * Sets the view to the given value.
+     *
+     * @param  newValue       the new value (may be {@code null}).
+     * @param  visibleBounds  if the property is an image, currently visible region. Can be {@code null}.
+     */
+    public void set(final Object newValue, final Rectangle visibleBounds) {
+        if (newValue != value || !Objects.equals(visibleBounds, visibleImageBounds)) {
+            if (runningTask != null) {
+                runningTask.cancel();
+                runningTask = null;
+            }
+            visibleImageBounds = visibleBounds;
+            final Node content;
+            if (newValue == null) {
+                content = null;
+            } else if (newValue instanceof RenderedImage) {
+                content = setImage((RenderedImage) newValue);
+            } else if (newValue instanceof Throwable) {
+                content = setText((Throwable) newValue);
+            } else {
+                content = setText(formatValue(newValue));
+            }
+            view.set(content);
+            value = newValue;           // Assign only on success.
+        }
+    }
+
+    /**
+     * Sets the property value to the given text.
+     */
+    private Node setText(final String text) {
+        TextArea node = textView;
+        if (node == null) {
+            node = new TextArea();
+            node.setEditable(false);
+            node.setFont(Font.font("Monospaced"));
+            textView = node;
+        }
+        node.setText(text);
+        return node;
+    }
+
+    /**
+     * Sets the text to the stack trace of given exception.
+     */
+    private Node setText(final Throwable ex) {
+        final StringWriter out = new StringWriter();
+        ex.printStackTrace(new PrintWriter(out));
+        return setText(out.toString());
+    }
+
+    /**
+     * Sets the property value to the given image.
+     */
+    private Node setImage(final RenderedImage image) {
+        ImageView node = imageView;
+        if (node == null) {
+            node = new ImageView();
+            node.setPreserveRatio(true);
+            imageCanvas.getChildren().setAll(node);
+            GUIUtilities.setClipToBounds(imageCanvas);
+            GridPane.setConstraints(imageCanvas, 0, 0, 2, 1);
+            GridPane.setHgrow(imageCanvas, Priority.ALWAYS);
+            GridPane.setVgrow(imageCanvas, Priority.ALWAYS);
+            final Vocabulary vocabulary = Vocabulary.getResources(getLocale());
+            final Label label = new Label(vocabulary.getLabel(Vocabulary.Keys.ValueRange));
+            label.setLabelFor(sampleValueRange = new Label());
+            imagePane = Styles.createControlGrid(1, label);
+            imagePane.getChildren().add(imageCanvas);
+            imageView = node;
+        }
+        final ImageConverter converter = new ImageConverter(image, visibleImageBounds, node);
+        converter.setOnSucceeded((e) -> taskCompleted(converter.getValue()));
+        converter.setOnFailed((e) -> {
+            taskCompleted(null);
+            view.set(setText(e.getSource().getException()));
+        });
+        runningTask = converter;
+        BackgroundThreads.execute(converter);
+        return imagePane;
+    }
+
+    /**
+     * Invoked when {@link #runningTask} completed its work, either successfully or with a failure.
+     */
+    private void taskCompleted(final Statistics[] statistics) {
+        runningTask = null;
+        String text = null;
+        if (statistics != null && statistics.length != 0) {
+            final Statistics s = statistics[0];
+            final FieldPosition pos = new FieldPosition(0);
+            final StringBuffer buffer = new StringBuffer();
+            final Format f = getFormat(Number.class);
+            format(f, s.minimum(), buffer, pos); buffer.append(" … ");
+            format(f, s.maximum(), buffer, pos);
+            text = buffer.toString();
+        }
+        sampleValueRange.setText(text);
+    }
+
+    /**
+     * Clears all content. This can be used for giving a chance to the garbage collector to release memory.
+     */
+    public void clear() {
+        value = null;
+        view.set(null);
+        if (textView  != null) textView .setText (null);
+        if (imageView != null) imageView.setImage(null);
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index 72840fa..c76c6e4 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -61,6 +61,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short AllFiles = 1;
 
         /**
+         * Along {0}
+         */
+        public static final short Along_1 = 35;
+
+        /**
          * Can not close “{0}”. Data may be lost.
          */
         public static final short CanNotClose_1 = 2;
@@ -116,6 +121,16 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CopyAs = 12;
 
         /**
+         * Display start
+         */
+        public static final short DisplayStart = 38;
+
+        /**
+         * Displayed size
+         */
+        public static final short DisplayedSize = 41;
+
+        /**
          * Does not cover the area of interest.
          */
         public static final short DoesNotCoverAOI = 13;
@@ -166,6 +181,16 @@ public final class Resources extends IndexedResourceBundle {
         public static final short GeospatialFiles = 23;
 
         /**
+         * Image start
+         */
+        public static final short ImageStart = 36;
+
+        /**
+         * {0} – inconsistency in {1}
+         */
+        public static final short InconsistencyIn_2 = 39;
+
+        /**
          * Loading…
          */
         public static final short Loading = 24;
@@ -196,16 +221,6 @@ public final class Resources extends IndexedResourceBundle {
         public static final short OpenDataFile = 29;
 
         /**
-         * Positional errors
-         */
-        public static final short PositionalErrors = 35;
-
-        /**
-         * Predefined filters
-         */
-        public static final short PredefinedFilters = 36;
-
-        /**
          * Select a coordinate reference system
          */
         public static final short SelectCRS = 30;
@@ -216,6 +231,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short SendTo = 31;
 
         /**
+         * Size or position
+         */
+        public static final short SizeOrPosition = 40;
+
+        /**
          * Standard error stream
          */
         public static final short StandardErrorStream = 32;
@@ -226,6 +246,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short TabularData = 33;
 
         /**
+         * Tile index start
+         */
+        public static final short TileIndexStart = 37;
+
+        /**
          * Visualize
          */
         public static final short Visualize = 34;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index d0e4fa4..e1d9cae 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -21,6 +21,7 @@
 #
 
 AllFiles               = All files
+Along_1                = Along {0}
 CanNotFetchTile_2      = Can not fetch tile ({0}, {1}).
 CanNotReadFile_1       = Can not open \u201c{0}\u201d.
 CanNotClose_1          = Can not close \u201c{0}\u201d. Data may be lost.
@@ -32,6 +33,8 @@ CanNotUseRefSys_1      = Can not use the \u201c{0}\u201d reference system.
 Close                  = Close
 Copy                   = Copy
 CopyAs                 = Copy as
+DisplayedSize          = Displayed size
+DisplayStart           = Display start
 DoesNotCoverAOI        = Does not cover the area of interest.
 ErrorDetails           = Details about error
 ErrorExportingData     = Error exporting data
@@ -42,17 +45,19 @@ ErrorDataAccess        = Error during data access
 Exit                   = Exit
 FullScreen             = Full screen
 GeospatialFiles        = Geospatial data files
+ImageStart             = Image start
+InconsistencyIn_2      = {0} \u2013 inconsistency in {1}
 Loading                = Loading\u2026
 MainWindow             = Main window
 NewWindow              = New window
 NoFeatureTypeInfo      = No feature type information.
 Open                   = Open\u2026
 OpenDataFile           = Open data file
-PositionalErrors       = Positional errors
-PredefinedFilters      = Predefined filters
 SelectCRS              = Select a coordinate reference system
 SendTo                 = Send to
+SizeOrPosition         = Size or position
 StandardErrorStream    = Standard error stream
 TabularData            = Tabular data
+TileIndexStart         = Tile index start
 Visualize              = Visualize
 Windows                = Windows
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index 9cb8dba..9d1faf5 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -26,6 +26,7 @@
 #
 
 AllFiles               = Tous les fichiers
+Along_1                = Selon {0}
 CanNotFetchTile_2      = Ne peut pas obtenir la tuile ({0}, {1}).
 CanNotReadFile_1       = Ne peut pas ouvrir \u00ab\u202f{0}\u202f\u00bb.
 CanNotClose_1          = Ne peut pas fermer \u00ab\u202f{0}\u202f\u00bb. Il pourrait y avoir une perte de donn\u00e9es.
@@ -37,6 +38,8 @@ CanNotUseRefSys_1      = Ne peut pas utiliser le syst\u00e8me de r\u00e9f\u00e9r
 Close                  = Fermer
 Copy                   = Copier
 CopyAs                 = Copier comme
+DisplayedSize          = Taille affich\u00e9e
+DisplayStart           = D\u00e9but de l\u2019affichage
 DoesNotCoverAOI        = Ne couvre pas la r\u00e9gion d\u2019int\u00e9r\u00eat.
 ErrorDetails           = D\u00e9tails \u00e0 propos de l\u2019erreur
 ErrorExportingData     = Erreur \u00e0 l\u2019exportation de donn\u00e9es
@@ -47,17 +50,19 @@ ErrorDataAccess        = Erreur lors de l\u2019acc\u00e8s \u00e0 la donn\u00e9e
 Exit                   = Quitter
 FullScreen             = Plein \u00e9cran
 GeospatialFiles        = Fichiers de donn\u00e9es g\u00e9ospatiales
+ImageStart             = D\u00e9but de l\u2019image
+InconsistencyIn_2      = {0} \u2013 incoh\u00e9rence dans {1}
 Loading                = Chargement\u2026
 MainWindow             = Fen\u00eatre principale
 NewWindow              = Nouvelle fen\u00eatre
 NoFeatureTypeInfo      = Pas d\u2019information sur le type d\u2019entit\u00e9.
 Open                   = Ouvrir\u2026
 OpenDataFile           = Ouvrir un fichier de donn\u00e9es
-PositionalErrors       = Erreurs de positionnement
-PredefinedFilters      = Filtres pr\u00e9d\u00e9finis
 SelectCRS              = Choisir un syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es
 SendTo                 = Envoyer vers
+SizeOrPosition         = Taille ou position
 StandardErrorStream    = Flux d\u2019erreur standard
 TabularData            = Tableau de valeurs
+TileIndexStart         = D\u00e9but des indices de tuiles
 Visualize              = Visualiser
 Windows                = Fen\u00eatres
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 d4aacbe..1c4c409 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
@@ -585,6 +585,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short ImageLayout = 103;
 
         /**
+         * Image size
+         */
+        public static final short ImageSize = 234;
+
+        /**
          * Implementation
          */
         public static final short Implementation = 104;
@@ -635,6 +640,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Latitude = 112;
 
         /**
+         * Layout
+         */
+        public static final short Layout = 235;
+
+        /**
          * Legend
          */
         public static final short Legend = 113;
@@ -805,6 +815,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short NumberOfNaN = 145;
 
         /**
+         * Number of tiles
+         */
+        public static final short NumberOfTiles = 236;
+
+        /**
          * Number of values
          */
         public static final short NumberOfValues = 146;
@@ -890,6 +905,16 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Projected = 162;
 
         /**
+         * Properties
+         */
+        public static final short Properties = 237;
+
+        /**
+         * Property
+         */
+        public static final short Property = 238;
+
+        /**
          * Publication date
          */
         public static final short PublicationDate = 163;
@@ -975,11 +1000,6 @@ 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 5c6b5d3..be15b03 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
@@ -119,6 +119,7 @@ Identifier              = Identifier
 Identifiers             = Identifiers
 Identity                = Identity
 Image                   = Image
+ImageSize               = Image size
 ImageLayout             = Image layout
 Implementation          = Implementation
 InBetweenWords          = \u2002in\u2002
@@ -131,6 +132,7 @@ JavaHome                = Java home directory
 Julian                  = Julian
 Latitude                = Latitude
 Longitude               = Longitude
+Layout                  = Layout
 Legend                  = Legend
 Level                   = Level
 Libraries               = Libraries
@@ -163,6 +165,7 @@ None                    = None
 Note                    = Note
 NorthBound              = North bound
 NumberOfDimensions      = Number of dimensions
+NumberOfTiles           = Number of tiles
 NumberOfValues          = Number of values
 NumberOfNaN             = Number of \u2018NaN\u2019
 Obligation              = Obligation
@@ -181,6 +184,8 @@ Paths                   = Paths
 Plugins                 = Plug-ins
 Preprocessing           = Preprocessing
 Projected               = Projected
+Property                = Property
+Properties              = Properties
 PublicationDate         = Publication date
 Purpose                 = Purpose
 Quoted_1                = \u201c{0}\u201d
@@ -198,7 +203,6 @@ 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 183e0b3..ab11e59 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
@@ -126,6 +126,7 @@ Identifier              = Identifiant
 Identifiers             = Identifiants
 Identity                = Identit\u00e9
 Image                   = Image
+ImageSize               = Taille de l\u2019image
 ImageLayout             = Agencement de l\u2019image
 Implementation          = Impl\u00e9mentation
 InBetweenWords          = \u2002dans\u2002
@@ -138,6 +139,7 @@ JavaHome                = R\u00e9pertoire du Java
 Julian                  = Julien
 Latitude                = Latitude
 Longitude               = Longitude
+Layout                  = Disposition
 Legend                  = L\u00e9gende
 Level                   = Niveau
 Libraries               = Biblioth\u00e8ques
@@ -170,6 +172,7 @@ None                    = Aucun
 Note                    = Note
 NorthBound              = Limite nord
 NumberOfDimensions      = Nombre de dimensions
+NumberOfTiles           = Nombre de tuiles
 NumberOfValues          = Nombre de valeurs
 NumberOfNaN             = Nombre de \u2018NaN\u2019
 Obligation              = Obligation
@@ -188,6 +191,8 @@ Paths                   = Chemins
 Plugins                 = Modules d\u2019extension
 Preprocessing           = Pr\u00e9traitement
 Projected               = Projet\u00e9
+Property                = Propri\u00e9t\u00e9
+Properties              = Propri\u00e9t\u00e9s
 PublicationDate         = Date de publication
 Purpose                 = Objectif
 Quoted_1                = \u00ab\u202f{0}\u202f\u00bb
@@ -205,7 +210,6 @@ 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