This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit b6f2655fcf2189f7e38a966cd13163565cc152e0
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Nov 15 16:43:06 2021 +0100

    Dissociate the process loading data in `GridView` and `CoverageCanvas`.
    Previous version was systematically loading data in `GridView` (even if it 
was not visible)
    then gave the `GridCoverage` reference to `CoverageCanvas` in order to load 
data only once.
    This new version loads data independently; we will rely more on `DataStore` 
resource caching.
    This separation is needed because `GridView` and `CoverageCanvas` will not 
load the same data
    anymore after we take pyramid in account. It also allows to create 
`GridView` only if needed.
---
 .../sis/gui/coverage/BandSelectionListener.java    |  16 +-
 .../org/apache/sis/gui/coverage/CellFormat.java    |   6 +-
 .../apache/sis/gui/coverage/CoverageControls.java  | 192 ++++++++-------------
 .../apache/sis/gui/coverage/CoverageExplorer.java  | 123 +++++++------
 .../org/apache/sis/gui/coverage/GridControls.java  |  47 ++---
 .../java/org/apache/sis/gui/coverage/GridView.java | 138 +++++++--------
 .../org/apache/sis/gui/coverage/ImageRequest.java  | 121 +++++++------
 .../sis/gui/coverage/InterpolationConverter.java   | 112 ++++++++++++
 .../sis/gui/coverage/PropertyPaneCreator.java      |  65 +++++++
 .../apache/sis/gui/coverage/ViewAndControls.java   |  39 +++--
 .../apache/sis/gui/dataset/ResourceExplorer.java   | 185 ++++++++++----------
 .../org/apache/sis/gui/dataset/SelectedData.java   |   7 +-
 .../org/apache/sis/gui/dataset/WindowManager.java  |   2 +-
 .../apache/sis/gui/metadata/MetadataSummary.java   |   1 -
 14 files changed, 607 insertions(+), 447 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java
index 8cc743e..6c394a2 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java
@@ -19,6 +19,7 @@ package org.apache.sis.gui.coverage;
 import javafx.beans.property.IntegerProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
+import javafx.scene.control.SelectionModel;
 
 
 /**
@@ -26,12 +27,23 @@ import javafx.beans.value.ObservableValue;
  * the selection is forwarded to the {@link GridView#bandProperty}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
 final class BandSelectionListener implements ChangeListener<Number> {
     /**
+     * Applies a bidirectional binding between a property and the selection in 
a tabme of sample dimensions.
+     *
+     * @param  bandProperty   the property for currently selected band.
+     * @param  bandSelection  the selection in a table of bands or sample 
dimensions.
+     */
+    static void bind(final IntegerProperty bandProperty, final 
SelectionModel<?> bandSelection) {
+        bandSelection.selectedIndexProperty().addListener(new 
BandSelectionListener(bandProperty));
+        bandProperty.addListener((p,o,n) -> 
bandSelection.clearAndSelect(n.intValue()));
+    }
+
+    /**
      * The {@link GridView#bandProperty} to update when a new band is selected.
      */
     private final IntegerProperty bandProperty;
@@ -45,7 +57,7 @@ final class BandSelectionListener implements 
ChangeListener<Number> {
     /**
      * Creates a new listener which will modify the given property.
      */
-    BandSelectionListener(final IntegerProperty bandProperty) {
+    private BandSelectionListener(final IntegerProperty bandProperty) {
         this.bandProperty = bandProperty;
     }
 
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
index 67b67a5..7e416e3 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
@@ -96,7 +96,7 @@ final class CellFormat extends SimpleStringProperty {
      * method to invoke, which {@code NumberFormat.format(…)} method to 
invoke, and whether to set a format pattern
      * with fraction digits.
      */
-    boolean dataTypeisInteger;
+    boolean dataTypeIsInteger;
 
     /**
      * Temporarily set to {@code true} when the user selects or enters a new 
pattern in a GUI control, then
@@ -231,7 +231,7 @@ final class CellFormat extends SimpleStringProperty {
      * @param  band   index of the band to show in this grid view.
      */
     final void configure(final RenderedImage image, final int band) {
-        if (dataTypeisInteger) {
+        if (dataTypeIsInteger) {
             cellFormat.setMaximumFractionDigits(0);
         } else {
             int n = getFractionDigits(image, band).orElse(1);
@@ -285,7 +285,7 @@ final class CellFormat extends SimpleStringProperty {
      */
     final String format(final Raster tile, final int x, final int y, final int 
b) {
         buffer.setLength(0);
-        if (dataTypeisInteger) {
+        if (dataTypeIsInteger) {
             final int  integer = tile.getSample(x, y, b);
             final double value = integer;
             if (Double.doubleToRawLongBits(value) != 
Double.doubleToRawLongBits(lastValue)) {
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 161bcc5..1f5d9ca 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
@@ -16,10 +16,8 @@
  */
 package org.apache.sis.gui.coverage;
 
-import java.util.List;
 import java.util.Locale;
-import java.util.Objects;
-import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
 import javafx.scene.control.Accordion;
 import javafx.scene.control.Control;
 import javafx.scene.control.TitledPane;
@@ -27,31 +25,29 @@ import javafx.scene.layout.BorderPane;
 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.collections.ObservableList;
+import javafx.concurrent.Task;
 import javafx.scene.control.Label;
 import javafx.scene.control.TableView;
 import javafx.scene.control.Tooltip;
 import javafx.scene.paint.Color;
-import javafx.util.StringConverter;
-import org.apache.sis.storage.Resource;
 import org.apache.sis.coverage.Category;
-import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.gui.referencing.RecentReferenceSystems;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.gui.map.MapMenu;
 import org.apache.sis.gui.map.StatusBar;
-import org.apache.sis.image.Interpolation;
+import org.apache.sis.internal.gui.control.ValueColorMapper;
+import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.internal.gui.control.ValueColorMapper;
 
 
 /**
  * A {@link CoverageCanvas} with associated controls to show in a {@link 
CoverageExplorer}.
+ * This class installs bidirectional bindings between {@link CoverageCanvas} 
and the controls.
+ * The controls are updated when the coverage shown in {@link CoverageCanvas} 
is changed.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
@@ -87,23 +83,22 @@ final class CoverageControls extends ViewAndControls {
     /**
      * Creates a new set of coverage controls.
      *
-     * @param  vocabulary  localized set of words, provided in argument 
because often known by the caller.
-     * @param  coverage    property containing the coverage to show.
+     * @param  owner  the widget which create this view. Can not be null.
      */
-    @SuppressWarnings("ThisEscapedInObjectConstruction")
-    CoverageControls(final Vocabulary vocabulary, final 
ObjectProperty<GridCoverage> coverage,
-                     final RecentReferenceSystems referenceSystems)
-    {
-        final Resources resources = 
Resources.forLocale(vocabulary.getLocale());
-        view = new CoverageCanvas(vocabulary.getLocale());
+    CoverageControls(final CoverageExplorer owner) {
+        super(owner);
+        final Locale     locale     = owner.getLocale();
+        final Resources  resources  = Resources.forLocale(locale);
+        final Vocabulary vocabulary = Vocabulary.getResources(locale);
+
+        view = new CoverageCanvas(locale);
         view.setBackground(Color.BLACK);
-        final StatusBar statusBar = new StatusBar(referenceSystems, view);
-        view.statusBar = statusBar;
+        view.statusBar = new StatusBar(owner.referenceSystems, view);
         imageAndStatus = new BorderPane(view.getView());
-        imageAndStatus.setBottom(statusBar.getView());
+        imageAndStatus.setBottom(view.statusBar.getView());
         final MapMenu menu = new MapMenu(view);
-        menu.addReferenceSystems(referenceSystems);
-        menu.addCopyOptions(statusBar);
+        menu.addReferenceSystems(owner.referenceSystems);
+        menu.addCopyOptions(view.statusBar);
         /*
          * "Display" section with the following controls:
          *    - Current CRS
@@ -121,7 +116,7 @@ final class CoverageControls extends ViewAndControls {
              *   - Interpolation
              */
             final GridPane valuesControl = Styles.createControlGrid(0,
-                label(vocabulary, Vocabulary.Keys.Interpolation, 
createInterpolationButton(vocabulary.getLocale())));
+                label(vocabulary, Vocabulary.Keys.Interpolation, 
InterpolationConverter.button(view)));
             final Label valuesHeader = labelOfGroup(vocabulary, 
Vocabulary.Keys.Values, valuesControl, false);
             /*
              * All sections put together.
@@ -164,120 +159,75 @@ final class CoverageControls extends ViewAndControls {
         final TitledPane p4 = new 
TitledPane(vocabulary.getString(Vocabulary.Keys.Properties), null);
         controls = new Accordion(p1, p2, p3, p4);
         controls.setExpandedPane(p1);
-        view.coverageProperty.bind(coverage);
+        view.coverageProperty.addListener((p,o,n) -> coverageChanged(null, n));
         p4.expandedProperty().addListener(new PropertyPaneCreator(view, p4));
     }
 
     /**
-     * Creates the controls for choosing an interpolation method.
+     * Invoked in JavaFX thread after {@link 
CoverageCanvas#setCoverage(GridCoverage)}.
+     * This method updates the GUI with new information available.
+     *
+     * @param  source  the new source of coverage, or {@code null} if none.
+     * @param  data    the new coverage, or {@code null} if none.
      */
-    private ChoiceBox<Interpolation> createInterpolationButton(final Locale 
locale) {
-        final ChoiceBox<Interpolation> b = new ChoiceBox<>();
-        b.setConverter(new InterpolationConverter(locale));
-        b.getItems().setAll(InterpolationConverter.INTERPOLATIONS);
-        b.getSelectionModel().select(view.getInterpolation());
-        
view.interpolationProperty.bind(b.getSelectionModel().selectedItemProperty());
-        return b;
+    private void coverageChanged(final Resource source, final GridCoverage 
data) {
+        final ObservableList<Category> items = categoryTable.getItems();
+        if (data == null) {
+            items.clear();
+        } else {
+            final int visibleBand = 0;          // TODO: provide a selector 
for the band to show.
+            
items.setAll(data.getSampleDimensions().get(visibleBand).getCategories());
+        }
+        owner.coverageChanged(source, data);
     }
 
     /**
-     * Gives a localized {@link String} instance for a given {@link 
Interpolation} and conversely.
+     * Sets the view content to the given coverage.
+     * This method starts a background thread.
+     *
+     * @param  request  the coverage to set, or {@code null} for clearing the 
view.
      */
-    private static final class InterpolationConverter extends 
StringConverter<Interpolation> {
-        /** The interpolation supported by this converter. */
-        static final Interpolation[] INTERPOLATIONS = {
-            Interpolation.NEAREST, Interpolation.BILINEAR, 
Interpolation.LANCZOS
-        };
-
-        /** Keys of localized names for each {@link #INTERPOLATIONS} element. 
*/
-        private static final short[] VOCABULARIES = {
-            Vocabulary.Keys.NearestNeighbor, Vocabulary.Keys.Bilinear, 0
-        };
-
-        /** The locale to use for string representation. */
-        private final Locale locale;
-
-        /** Creates a new converter for the given locale. */
-        InterpolationConverter(final Locale locale) {
-            this.locale = locale;
-        }
-
-        /** Returns a string representation of the given item. */
-        @Override public String toString(final Interpolation item) {
-            for (int i=0; i<INTERPOLATIONS.length; i++) {
-                if (INTERPOLATIONS[i].equals(item)) {
-                    final short key = VOCABULARIES[i];
-                    if (key != 0) {
-                        return Vocabulary.getResources(locale).getString(key);
-                    } else if (item == Interpolation.LANCZOS) {
-                        return "Lanczos";
-                    }
-                }
-            }
-            return Objects.toString(item);
-        }
-
-        /** Returns the interpolation for the given text. */
-        @Override public Interpolation fromString(final String text) {
-            final Vocabulary vocabulary = Vocabulary.getResources(locale);
-            for (int i=0; i<VOCABULARIES.length; i++) {
-                final short key = VOCABULARIES[i];
-                final Interpolation item = INTERPOLATIONS[i];
-                if ((key != 0 && 
vocabulary.getString(key).equalsIgnoreCase(text))
-                                        || 
item.toString().equalsIgnoreCase(text))
-                {
-                    return item;
-                }
-            }
-            return null;
+    @Override
+    final void load(final ImageRequest request) {
+        if (request == null) {
+            view.setOriginator(null);
+            view.setCoverage(null);
+        } else {
+            view.setOriginator(request.resource != null ? new 
WeakReference<>(request.resource) : null);
+            request.getCoverage().ifPresentOrElse(view::setCoverage,
+                    () -> BackgroundThreads.execute(new Loader(request)));
         }
     }
 
     /**
-     * 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.
+     * A task for loading {@link GridCoverage} from a resource in a background 
thread.
+     *
+     * @todo Remove this loader, replace by a {@code resourceProperty} in 
{@link CoverageCanvas}.
      */
-    private static final class PropertyPaneCreator implements 
ChangeListener<Boolean> {
-        /** A copy of {@link CoverageControls#view} reference. */
-        private final CoverageCanvas view;
+    private final class Loader extends Task<GridCoverage> {
+        /** The coverage resource together with optional parameters for 
reading only a subset. */
+        private final ImageRequest request;
 
-        /** The pane where to set the content. */
-        private final TitledPane pane;
+        /** Creates a new task for loading a coverage from the specified 
resource. */
+        Loader(final ImageRequest request) {
+            this.request = request;
+        }
 
-        /** Creates a new {@link ImagePropertyExplorer} constructor. */
-        PropertyPaneCreator(final CoverageCanvas view, final TitledPane pane) {
-            this.view = view;
-            this.pane = pane;
+        /** Invoked in background thread for loading the coverage. */
+        @Override protected GridCoverage call() throws DataStoreException {
+            request.load(this, true, false);
+            return request.getCoverage().orElse(null);
         }
 
-        /** Creates the {@link ImagePropertyExplorer} when {@link 
TitledPane#expandedProperty()} changed. */
-        @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 successful loading. */
+        @Override protected void succeeded() {
+            view.setCoverage(getValue());
         }
-    }
 
-    /**
-     * Invoked in JavaFX thread after {@link 
CoverageExplorer#setCoverage(ImageRequest)} completed.
-     * This method updates the GUI with new information available.
-     *
-     * @param  data        the new coverage, or {@code null} if none.
-     * @param  originator  the resource from which the data has been read, or 
{@code null} if unknown.
-     */
-    @Override
-    final void coverageChanged(final GridCoverage data, final 
Reference<Resource> originator) {
-        view.setOriginator(originator);
-        if (data != null) {
-            final int visibleBand = 0;          // TODO: provide a selector 
for the band to show.
-            final List<SampleDimension> bands = data.getSampleDimensions();
-            
categoryTable.getItems().setAll(bands.get(visibleBand).getCategories());
-        } else {
-            categoryTable.getItems().clear();
+        /** Invoked in JavaFX thread on failure. */
+        @Override protected void failed() {
+            view.setCoverage(null);
+            request.reportError(imageAndStatus, getException());
         }
     }
 
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index a06d273..698945a 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -17,8 +17,7 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.EnumMap;
-import java.lang.ref.Reference;
-import java.lang.ref.WeakReference;
+import java.awt.image.RenderedImage;
 import javafx.application.Platform;
 import javafx.beans.DefaultProperty;
 import javafx.scene.control.Control;
@@ -36,8 +35,6 @@ import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.ToolbarButton;
 import org.apache.sis.internal.gui.NonNullObjectProperty;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.gui.Widget;
@@ -49,7 +46,8 @@ import org.apache.sis.storage.Resource;
  * The class contains two properties:
  *
  * <ul>
- *   <li>A {@link GridCoverage} supplied by user, or an {@link ImageRequest} 
for loading a coverage.</li>
+ *   <li>A {@link GridCoverage} supplied by user.
+ *       May be specified indirectly with an {@link ImageRequest} for loading 
the coverage.</li>
  *   <li>A {@link View} type which specify how to show the coverage:
  *     <ul>
  *       <li>using {@link GridView} for showing numerical values in a table, 
or</li>
@@ -151,6 +149,7 @@ public class CoverageExplorer extends Widget {
      * The type of control may change in any future SIS version.
      *
      * @see #getView()
+     * @see #onViewTypeSpecified(View)
      */
     private SplitPane content;
 
@@ -166,7 +165,7 @@ public class CoverageExplorer extends Widget {
     /**
      * Handles the {@link javafx.scene.control.ChoiceBox} and menu items for 
selecting a CRS.
      */
-    private final RecentReferenceSystems referenceSystems;
+    final RecentReferenceSystems referenceSystems;
 
     /**
      * Creates an initially empty explorer with default view type.
@@ -195,6 +194,7 @@ public class CoverageExplorer extends Widget {
         ArgumentChecks.ensureNonNull("type", type);
         coverageProperty = new SimpleObjectProperty<>(this, "coverage");
         viewTypeProperty = new NonNullObjectProperty<>(this, "viewType", type);
+        viewTypeProperty.addListener((p,o,n) -> onViewTypeSpecified(n));
         coverageProperty.addListener((p,o,n) -> onCoverageSpecified(n));
         referenceSystems = new RecentReferenceSystems();
         referenceSystems.addUserPreferences();
@@ -210,19 +210,33 @@ public class CoverageExplorer extends Widget {
     /**
      * Returns the view-control pair for the given view type.
      * The view-control pair is created when first needed.
+     *
+     * @param  type  type of view to obtain.
+     * @param  load  whether to force loading of data in the new type.
      */
-    private ViewAndControls getViewAndControls(final View type) {
+    private ViewAndControls getViewAndControls(final View type, boolean load) {
         ViewAndControls c = views.get(type);
         if (c == null) {
-            final Vocabulary vocabulary = Vocabulary.getResources(getLocale());
             switch (type) {
-                case TABLE: c = new GridControls(referenceSystems, 
vocabulary); break;
-                case IMAGE: c = new CoverageControls(vocabulary, 
coverageProperty, referenceSystems); break;
+                case TABLE: c = new GridControls(this); break;
+                case IMAGE: c = new CoverageControls(this); break;
                 default: throw new AssertionError(type);
             }
             SplitPane.setResizableWithParent(c.controls(), Boolean.FALSE);
             SplitPane.setResizableWithParent(c.view(),     Boolean.TRUE);
             views.put(type, c);
+            load = true;
+        }
+        /*
+         * If this explorer is showing a coverage, load data in the newly 
created view.
+         * Data may also be loaded because the view was previously unselected 
(hidden)
+         * and became selected (visible).
+         */
+        if (load) {
+            final GridCoverage coverage = getCoverage();
+            if (coverage != null) {
+                c.load(new ImageRequest(coverage, null));
+            }
         }
         return c;
     }
@@ -258,11 +272,10 @@ public class CoverageExplorer extends Widget {
                 buttons[1 + type.ordinal()] = new 
Selector(type).createButton(group, type.icon, localized, type.tooltip);
             }
             final View type = getViewType();
-            final ViewAndControls c = getViewAndControls(type);
+            final ViewAndControls c = getViewAndControls(type, false);
             group.selectToggle(group.getToggles().get(type.ordinal()));
             content = new SplitPane(c.controls(), c.view());
             ToolbarButton.insert(content, buttons);
-            viewTypeProperty.addListener((p,o,n) -> onViewTypeSpecified(n));
             /*
              * The divider position is supposed to be a fraction between 0 and 
1. A value of 1 would mean
              * to give all the space to controls and no space to data, which 
is not what we want. However
@@ -287,7 +300,7 @@ public class CoverageExplorer extends Widget {
     public final Region getDataView(final View type) {
         assert Platform.isFxApplicationThread();
         ArgumentChecks.ensureNonNull("type", type);
-        return getViewAndControls(type).view();
+        return getViewAndControls(type, false).view();
     }
 
     /**
@@ -301,7 +314,7 @@ public class CoverageExplorer extends Widget {
     public final Region getControls(final View type) {
         assert Platform.isFxApplicationThread();
         ArgumentChecks.ensureNonNull("type", type);
-        return getViewAndControls(type).controls();
+        return getViewAndControls(type, false).controls();
     }
 
     /**
@@ -321,7 +334,7 @@ public class CoverageExplorer extends Widget {
             final Toggle button = (Toggle) event.getSource();
             if (button.isSelected()) {
                 setViewType(type);
-                views.get(type).selector = button;    // Should never null 
null.
+                views.get(type).selector = button;          // Should never be 
null.
             } else {
                 button.setSelected(true);       // Prevent situation where all 
buttons are unselected.
             }
@@ -335,6 +348,8 @@ public class CoverageExplorer extends Widget {
      * @return the coverage shown in this explorer, or {@code null} if none.
      *
      * @see #coverageProperty
+     * @see CoverageCanvas#getCoverage()
+     * @see GridView#getImage()
      */
     public final GridCoverage getCoverage() {
         return coverageProperty.get();
@@ -349,9 +364,12 @@ public class CoverageExplorer extends Widget {
      * @param  coverage  the data to show in this explorer, or {@code null} if 
none.
      *
      * @see #coverageProperty
+     * @see CoverageCanvas#setCoverage(GridCoverage)
+     * @see GridView#setImage(RenderedImage)
      */
     public final void setCoverage(final GridCoverage coverage) {
         coverageProperty.set(coverage);
+        // `onCoverageSpecified(…)` is indirectly invoked.
     }
 
     /**
@@ -361,18 +379,19 @@ public class CoverageExplorer extends Widget {
      * the modifications will appear after an undetermined amount of time.
      *
      * @param  source  the coverage or resource to load, or {@code null} if 
none.
+     *
+     * @see GridView#setImage(ImageRequest)
      */
     public final void setCoverage(final ImageRequest source) {
         assert Platform.isFxApplicationThread();
-        if (source == null) {
-            setCoverage((GridCoverage) null);
-        } else if (source.listener != null) {
-            throw new IllegalArgumentException(Errors.getResources(getLocale())
-                    .getString(Errors.Keys.AlreadyInitialized_1, "listener"));
-        } else {
-            source.listener = this;
-            startLoading(source);
+        final ViewAndControls current = views.get(getViewType());
+        for (final ViewAndControls c : views.values()) {
+            c.load(c == current ? source : null);
+        }
+        if (current == null) {
+            coverageChanged(null, null);
         }
+        // Else `coverageChanged(…)` will be invoked later after background 
thread finishes its work.
     }
 
     /**
@@ -384,50 +403,19 @@ public class CoverageExplorer extends Widget {
      */
     private void onCoverageSpecified(final GridCoverage coverage) {
         if (!isCoverageAdjusting) {
-            startLoading(null);                                         // 
Clear data.
-            notifyCoverageChange(coverage, null);
-            if (coverage != null) {
-                startLoading(new ImageRequest(coverage, null));         // 
Start a background thread.
-            }
+            setCoverage((coverage != null) ? new ImageRequest(coverage, null) 
: null);
         }
     }
 
     /**
-     * Invoked in JavaFX thread by {@link GridView} after the coverage has 
been read.
-     *
-     * @param  coverage    the new coverage, or {@code null} if loading failed 
or has been cancelled.
-     * @param  originator  resource from which the data has been read, or 
{@code null} if unknown.
-     */
-    final void onCoverageLoaded(final GridCoverage coverage, final Resource 
originator) {
-        notifyCoverageChange(coverage, (originator != null) ? new 
WeakReference<>(originator) : null);
-        isCoverageAdjusting = true;
-        try {
-            setCoverage(coverage);
-        } finally {
-            isCoverageAdjusting = false;
-        }
-    }
-
-    /**
-     * Invoked by {@link #setCoverage(ImageRequest)} for starting data loading 
in a background thread.
-     * This method is invoked in JavaFX thread.
-     *
-     * @param  source  the coverage or resource to load, or {@code null} if 
none.
-     */
-    private void startLoading(final ImageRequest source) {
-        final GridView main = (GridView) getViewAndControls(View.TABLE).view();
-        main.setImage(source);
-    }
-
-    /**
      * Invoked in JavaFX thread after {@link #setCoverage(ImageRequest)} 
completion for notifying controls
      * about the coverage change. Controls should update the GUI with new 
information available,
      * in particular the coordinate reference system and the list of sample 
dimensions.
      *
-     * @param  data        the new coverage, or {@code null} if none.
-     * @param  originator  the resource from which the data has been read, or 
{@code null} if unknown.
+     * @param  source  the new source of coverage, or {@code null} if none.
+     * @param  data    the new coverage, or {@code null} if none.
      */
-    private void notifyCoverageChange(final GridCoverage data, final 
Reference<Resource> originator) {
+    final void coverageChanged(final Resource source, final GridCoverage data) 
{
         if (data != null) {
             final GridGeometry gg = data.getGridGeometry();
             
referenceSystems.areaOfInterest.set(gg.isDefined(GridGeometry.ENVELOPE) ? 
gg.getEnvelope() : null);
@@ -435,8 +423,11 @@ public class CoverageExplorer extends Widget {
                 referenceSystems.setPreferred(true, 
gg.getCoordinateReferenceSystem());
             }
         }
-        for (final ViewAndControls c : views.values()) {
-            c.coverageChanged(data, originator);
+        isCoverageAdjusting = true;
+        try {
+            setCoverage(data);
+        } finally {
+            isCoverageAdjusting = false;
         }
     }
 
@@ -469,11 +460,13 @@ public class CoverageExplorer extends Widget {
      * @param  type  the new way to show coverages in this explorer.
      */
     private void onViewTypeSpecified(final View type) {
-        final ViewAndControls c = getViewAndControls(type);
-        content.getItems().setAll(c.controls(), c.view());
-        final Toggle selector = c.selector;
-        if (selector != null) {
-            selector.setSelected(true);
+        final ViewAndControls c = getViewAndControls(type, true);
+        if (content != null) {
+            content.getItems().setAll(c.controls(), c.view());
+            final Toggle selector = c.selector;
+            if (selector != null) {
+                selector.setSelected(true);
+            }
         }
     }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java
index 876eb97..d0d84b0 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.gui.coverage;
 
-import java.lang.ref.Reference;
 import javafx.beans.property.DoubleProperty;
 import javafx.collections.ObservableList;
 import javafx.scene.control.Accordion;
@@ -31,13 +30,14 @@ import javafx.scene.layout.VBox;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.gui.Styles;
 
 
 /**
  * A {@link GridView} with associated controls to show in a {@link 
CoverageExplorer}.
+ * This class installs bidirectional bindings between {@link GridView} and the 
controls.
+ * The controls are updated when the image shown in {@link GridView} is 
changed.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
@@ -63,14 +63,14 @@ final class GridControls extends ViewAndControls {
     /**
      * Creates a new set of grid controls.
      *
-     * @param  referenceSystems  the manager of reference systems chosen by 
the user, or {@code null} if none.
-     * @param  vocabulary        localized set of words, provided in argument 
because often known by the caller.
+     * @param  owner  the widget which create this view. Can not be null.
      */
-    GridControls(final RecentReferenceSystems referenceSystems, final 
Vocabulary vocabulary) {
-        view = new GridView(referenceSystems);
+    GridControls(final CoverageExplorer owner) {
+        super(owner);
+        view = new GridView(this, owner.referenceSystems);
+        final Vocabulary vocabulary = 
Vocabulary.getResources(owner.getLocale());
         sampleDimensions = new 
BandRangeTable(view.cellFormat).create(vocabulary);
-        
sampleDimensions.getSelectionModel().selectedIndexProperty().addListener(new 
BandSelectionListener(view.bandProperty));
-        view.bandProperty.addListener((p,o,n) -> onBandSpecified(n));
+        BandSelectionListener.bind(view.bandProperty, 
sampleDimensions.getSelectionModel());
         /*
          * "Coverage" section with the following controls:
          *    - Coverage domain as a list of CRS dimensions with two of them 
selected (TODO).
@@ -119,22 +119,13 @@ final class GridControls extends ViewAndControls {
     }
 
     /**
-     * Invoked when the band property changed. This method ensures that the 
selected row
-     * in the sample dimension table matches the band which is shown in the 
grid view.
-     */
-    private void onBandSpecified(final Number band) {
-        sampleDimensions.getSelectionModel().clearAndSelect(band.intValue());
-    }
-
-    /**
-     * Invoked after {@link CoverageExplorer#setCoverage(ImageRequest)} for 
updating the table of
-     * sample dimensions when information become available. This method is 
invoked in JavaFX thread.
+     * Invoked after {@link GridView#setImage(ImageRequest)} for updating the 
table of sample
+     * dimensions when information become available. This method is invoked in 
JavaFX thread.
      *
-     * @param  data        the new coverage, or {@code null} if none.
-     * @param  originator  the resource from which the data has been read, or 
{@code null} if unknown.
+     * @param  source  the new source of coverage, or {@code null} if none.
+     * @param  data    the new coverage, or {@code null} if none.
      */
-    @Override
-    final void coverageChanged(final GridCoverage data, final 
Reference<Resource> originator) {
+    final void coverageChanged(final Resource source, final GridCoverage data) 
{
         final ObservableList<SampleDimension> items = 
sampleDimensions.getItems();
         if (data != null) {
             items.setAll(data.getSampleDimensions());
@@ -142,6 +133,18 @@ final class GridControls extends ViewAndControls {
         } else {
             items.clear();
         }
+        owner.coverageChanged(source, data);
+    }
+
+    /**
+     * Sets the view content to the given image.
+     * This method starts a background thread.
+     *
+     * @param  request  the image to set, or {@code null} for clearing the 
view.
+     */
+    @Override
+    final void load(final ImageRequest request) {
+        view.setImage(request);
     }
 
     /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
index 057c7e7..33ed675 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
@@ -38,10 +38,7 @@ import javafx.scene.paint.Paint;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.GridCoverageResource;
-import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.internal.gui.BackgroundThreads;
-import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
@@ -59,14 +56,12 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities;
  * consider using the standard JavaFX {@link javafx.scene.control.TableView} 
instead.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see CoverageExplorer
  *
  * @since 1.1
  * @module
- *
- * @todo Allow users to specify a {@link NumberFormat} pattern for writing 
sample values.
  */
 @DefaultProperty("image")
 public class GridView extends Control {
@@ -124,8 +119,8 @@ public class GridView extends Control {
     private final GridTileCache tiles;
 
     /**
-     * The most recently used tile. Cached separately because it will be the 
desired tile in the vast majority
-     * of cases.
+     * The most recently used tile.
+     * Cached separately because it will be the desired tile in the vast 
majority of cases.
      */
     private GridTile lastTile;
 
@@ -212,20 +207,31 @@ public class GridView extends Control {
     final StatusBar statusBar;
 
     /**
+     * If this grid view is associated with controls, the controls. Otherwise 
{@code null}.
+     * This is used only for notifications; a future version may use a more 
generic listener.
+     * We use this specific mechanism because there is no {@code 
coverageProperty} in this class.
+     *
+     * @see GridControls#coverageChanged(GridCoverage)
+     */
+    private final GridControls controls;
+
+    /**
      * Creates an initially empty grid view. The content can be set after
      * construction by a call to {@link #setImage(RenderedImage)}.
      */
     public GridView() {
-        this(null);
+        this(null, null);
     }
 
     /**
      * Creates an initially empty grid view. The content can be set after
      * construction by a call to {@link #setImage(RenderedImage)}.
      *
+     * @param  controls          the controls of this grid view, or {@code 
null} if none.
      * @param  referenceSystems  the manager of reference systems chosen by 
the user, or {@code null} if none.
      */
-    GridView(final RecentReferenceSystems referenceSystems) {
+    GridView(final GridControls controls, final RecentReferenceSystems 
referenceSystems) {
+        this.controls    = controls;
         bandProperty     = new BandProperty();
         imageProperty    = new SimpleObjectProperty<>(this, "image");
         headerWidth      = new SimpleDoubleProperty  (this, "headerWidth", 60);
@@ -299,6 +305,7 @@ public class GridView extends Control {
      */
     public final void setImage(final RenderedImage image) {
         imageProperty.set(image);
+        // Above call will cause an invocation of `onImageSpecified(image)`.
     }
 
     /**
@@ -308,24 +315,41 @@ public class GridView extends Control {
      * the modifications will appear after an undetermined amount of time.
      *
      * @param  source  the coverage or resource to load, or {@code null} if 
none.
+     *
+     * @see CoverageExplorer#setCoverage(ImageRequest)
      */
     public void setImage(final ImageRequest source) {
         if (source == null) {
             setImage((RenderedImage) null);
-        } else {
-            final ImageLoader previous = loader;
-            loader = null;
-            if (previous != null) {
-                previous.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO);
+            if (controls != null) {
+                controls.coverageChanged(null, null);
             }
-            loader = new ImageLoader(source, true);
+        } else {
+            cancelLoader();
+            loader = new ImageLoader(source);
             BackgroundThreads.execute(loader);
         }
     }
 
     /**
-     * A task for loading {@link GridCoverage} from a resource in a background 
thread,
-     * then fetching an image from it.
+     * Invoked after the image has been loaded or after failure.
+     *
+     * @param  source  the coverage or resource to load (never {@code null}).
+     * @param  image   the loaded image, or {@code null} on failure.
+     */
+    private void setLoadedImage(final ImageRequest request, final 
RenderedImage image) {
+        loader = null;          // Must be first for preventing cancellation.
+        setImage(image);
+        request.configure(statusBar);
+        if (controls != null) {
+            controls.coverageChanged(request.resource, 
request.getCoverage().orElse(null));
+        }
+    }
+
+    /**
+     * A task for loading {@link GridCoverage} from a resource in a background 
thread, then fetching an image from it.
+     *
+     * @see #setImage(ImageRequest)
      */
     private final class ImageLoader extends Task<RenderedImage> {
         /**
@@ -334,32 +358,24 @@ public class GridView extends Control {
         private final ImageRequest request;
 
         /**
-         * Whether the caller wants a grid coverage that contains real values 
or sample values.
-         */
-        private final boolean converted;
-
-        /**
          * Creates a new task for loading an image from the specified coverage 
resource.
          *
-         * @param  request    source of the image to load.
-         * @param  converted  {@code true} for a coverage containing converted 
values,
+         * @param  request  source of the image to load.
          */
-        ImageLoader(final ImageRequest request, final boolean converted) {
-            this.request   = request;
-            this.converted = converted;
+        ImageLoader(final ImageRequest request) {
+            this.request = request;
         }
 
         /**
-         * Loads the image. Current implementation reads the full image. If 
the coverage has more than 2 dimensions,
-         * only two of them are taken for the image; for all other dimensions, 
only the values at lowest index will
-         * be read.
+         * Loads the image. If the coverage has more than 2 dimensions, only 
two of them are taken for the image;
+         * for all other dimensions, only the values at lowest index will be 
read.
          *
          * @return the image loaded from the source given at construction time.
          * @throws DataStoreException if an error occurred while loading the 
grid coverage.
          */
         @Override
         protected RenderedImage call() throws DataStoreException {
-            return request.load(this, converted);
+            return request.load(this, true, true);
         }
 
         /**
@@ -368,10 +384,7 @@ public class GridView extends Control {
          */
         @Override
         protected void succeeded() {
-            loader = null;
-            terminated(request.getCoverage().get());    // Should not be empty 
when the task is successful.
-            setImage(getValue());                       // Must be after the 
coverage has been set.
-            request.configure(statusBar);
+            setLoadedImage(request, getValue());
         }
 
         /**
@@ -380,36 +393,8 @@ public class GridView extends Control {
          */
         @Override
         protected void failed() {
-            terminated(null);
-            setImage((RenderedImage) null);
-            final GridCoverageResource resource = request.resource;
-            final GridView owner = GridView.this;
-            if (resource instanceof StoreListeners) {
-                ExceptionReporter.canNotReadFile(owner, ((StoreListeners) 
resource).getSourceName(), getException());
-            } else {
-                ExceptionReporter.canNotUseResource(owner, getException());
-            }
-        }
-
-        /**
-         * Invoked in JavaFX thread in case of cancellation.
-         */
-        @Override
-        protected void cancelled() {
-            terminated(null);
-        }
-
-        /**
-         * Notifies listener that the given coverage has been read or failed 
to be read,
-         * then discards the listener. This method shall be invoked in JavaFX 
thread.
-         * A null argument means that the read operation failed (or has been 
cancelled.
-         */
-        private void terminated(final GridCoverage result) {
-            final CoverageExplorer snapshot = request.listener;
-            request.listener = null;                // Clear now in case an 
error happen.
-            if (snapshot != null) {
-                snapshot.onCoverageLoaded(result, request.resource);
-            }
+            setLoadedImage(request, null);
+            request.reportError(GridView.this, getException());
         }
     }
 
@@ -436,6 +421,18 @@ public class GridView extends Control {
     }
 
     /**
+     * If an image is loaded in a background thread, cancel the loading 
process.
+     * This method is invoked when a new image is specified.
+     */
+    private void cancelLoader() {
+        final ImageLoader previous = loader;
+        if (previous != null) {
+            loader = null;
+            previous.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO);
+        }
+    }
+
+    /**
      * Invoked (indirectly) when the user sets a new {@link RenderedImage}.
      * See {@link #setImage(RenderedImage)} for more description.
      *
@@ -443,10 +440,7 @@ public class GridView extends Control {
      * @throws ArithmeticException if the "tile grid x/y offset" property is 
too large.
      */
     private void onImageSpecified(final RenderedImage image) {
-        if (loader != null) {
-            loader.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO);
-            loader = null;
-        }
+        cancelLoader();
         tiles.clear();          // Let garbage collector dispose the rasters.
         lastTile = null;
         width    = 0;
@@ -461,14 +455,14 @@ public class GridView extends Control {
             tileHeight      = Math.max(1, image.getTileHeight());
             tileGridXOffset = Math.subtractExact(image.getTileGridXOffset(), 
minX);
             tileGridYOffset = Math.subtractExact(image.getTileGridYOffset(), 
minY);
-            cellFormat.dataTypeisInteger = false;           // To be kept 
consistent with `cellFormat` pattern.
+            cellFormat.dataTypeIsInteger = false;           // To be kept 
consistent with `cellFormat` pattern.
             final SampleModel sm = image.getSampleModel();
             if (sm != null) {                               // Should never be 
null, but we are paranoiac.
                 final int numBands = sm.getNumBands();
                 if (getBand() >= numBands) {
                     ((BandProperty) bandProperty).setNoCheck(numBands - 1);
                 }
-                cellFormat.dataTypeisInteger = 
ImageUtilities.isIntegerType(sm);
+                cellFormat.dataTypeIsInteger = 
ImageUtilities.isIntegerType(sm);
             }
             cellFormat.configure(image, getBand());
         }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
index dd3b5ba..df3566d 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
@@ -19,6 +19,7 @@ package org.apache.sis.gui.coverage;
 import java.util.Optional;
 import java.util.concurrent.FutureTask;
 import java.awt.image.RenderedImage;
+import javafx.scene.Node;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridDerivation;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -27,8 +28,10 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.internal.gui.LogHandler;
+import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -37,8 +40,12 @@ import org.apache.sis.storage.DataStoreException;
  * {@linkplain GridCoverage#render(GridExtent) rendering} and image in a 
background thread.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
+ * @version 1.2
+ *
+ * @see GridView#setImage(ImageRequest)
+ * @see CoverageExplorer#setCoverage(ImageRequest)
+ *
+ * @since 1.1
  * @module
  */
 public class ImageRequest {
@@ -49,16 +56,18 @@ public class ImageRequest {
 
     /**
      * The source from where to read the image, specified at construction time.
-     * Can not be {@code null}.
+     * May be {@code null} if {@link #coverage} instance was specified at 
construction time.
      */
     final GridCoverageResource resource;
 
     /**
      * The source for rendering the image, specified at construction time.
-     * After construction, only one of {@link #resource} and {@link #coverage} 
is non-null.
-     * But after task execution, this field will be set to the coverage which 
has been read.
+     * After construction, only one of {@link #resource} and {@code coverage} 
fields is non-null.
+     * But after {@link Loader} task execution, this field will be set to the 
coverage which has been read.
+     *
+     * @see #getCoverage()
      */
-    private GridCoverage coverage;
+    private volatile GridCoverage coverage;
 
     /**
      * Desired grid extent and resolution, or {@code null} for reading the 
whole domain.
@@ -72,7 +81,7 @@ public class ImageRequest {
      * 0-based indices of sample dimensions to read, or {@code null} for 
reading them all.
      * This is used only if the data source is a {@link GridCoverageResource}.
      *
-     * @see #getDomain()
+     * @see #getRange()
      */
     private final int[] range;
 
@@ -95,13 +104,6 @@ public class ImageRequest {
     private static final double SLICE_RATIO = 0;
 
     /**
-     * The coverage explorer to inform after loading completed, or {@code 
null} if none.
-     * We do not provide a more generic listeners API for now, but we could do 
that
-     * in the future if there is a need.
-     */
-    CoverageExplorer listener;
-
-    /**
      * Creates a new request for loading an image from the specified resource.
      * If {@code domain} and {@code range} arguments are null, then the full 
coverage will be loaded.
      * For loading a smaller amount of data, sub-domain or sub-range can be 
specified as documented
@@ -232,16 +234,18 @@ public class ImageRequest {
      * argument. This method is provided for the rare cases where it may be 
useful to specify both the {@code domain}
      * and the {@code sliceExtent}.</div>
      *
-     * @param  sliceExtent  subspace of the grid coverage extent to render.
+     * @param  sliceExtent  subspace of the grid coverage extent to render, or 
{@code null} for the whole extent.
      */
     public final void setSliceExtent(final GridExtent sliceExtent) {
         this.sliceExtent = sliceExtent;
     }
 
     /**
-     * Computes a two dimension slice of the given grid geometry.
+     * Computes a two dimensional slice of the given grid geometry.
      * This method selects the two first dimensions having a size greater than 
1 cell.
      *
+     * @todo Give control to user over which dimensions are selected.
+     *
      * @param  domain  the grid geometry in which to choose a two-dimensional 
slice.
      * @return a builder configured for returning the desired two-dimensional 
slice.
      */
@@ -260,53 +264,57 @@ public class ImageRequest {
     }
 
     /**
-     * Loads the image. Current implementation reads the full image. If the 
coverage has more than
-     * {@value #BIDIMENSIONAL} dimensions, only two of them are taken for the 
image; for all other
-     * dimensions, only the values at lowest index will be read.
+     * Loads the image. If the coverage has more than {@value #BIDIMENSIONAL} 
dimensions,
+     * only two of them are taken for the image; for all other dimensions, 
only the values
+     * at lowest index will be read.
      *
      * <p>If the {@link #coverage} field was null, it will be initialized as a 
side-effect.
      * No other fields will be modified.</p>
      *
-     * <p>This class does not need to be thread-safe since it should be used 
only once in a well-defined
-     * life cycle. We nevertheless synchronize as a safety (user could give 
the same {@code ImageRequest}
-     * to two different {@link CoverageExplorer} instances).</p>
+     * <h4>Thread safety</h4>
+     * This class does not need to be thread-safe because it should be used 
only once in a well-defined life cycle.
+     * We nevertheless synchronize as a safety (e.g. user could give the same 
{@code ImageRequest} to two different
+     * {@link CoverageExplorer} instances). In such case the {@link 
GridCoverage} will be loaded only once,
+     * but no caching is done for the {@link RenderedImage}. Image caching is 
generally not needed because
+     * {@link CoverageCanvas} does its own image rendering (it invokes this 
method with {@code render = false}).
+     * If two image renderings happen anyway, we rely on {@link 
org.apache.sis.storage.DataStore} caching.</p>
      *
      * @param  task       the task invoking this method (for checking for 
cancellation).
      * @param  converted  {@code true} for a coverage containing converted 
values,
      *                    or {@code false} for a coverage containing packed 
values.
-     * @return the image loaded from the source given at construction time,
-     *         or {@code null} if the task has been cancelled.
+     * @param  render     {@code false} if only coverage reading is desired.
+     * @return the image loaded from the source given at construction time, or 
{@code null}
+     *         if the task has been cancelled or if {@code render} is {@code 
false}.
      * @throws DataStoreException if an error occurred while loading the grid 
coverage.
      */
-    final synchronized RenderedImage load(final FutureTask<?> task, final 
boolean converted) throws DataStoreException {
+    final synchronized RenderedImage load(final FutureTask<?> task, final 
boolean converted, final boolean render)
+            throws DataStoreException
+    {
+        GridCoverage cv = coverage;
         final Long id = LogHandler.loadingStart(resource);
         try {
-            if (coverage == null) {
-                GridGeometry domain = this.domain;
-                if (domain == null) {
-                    domain = resource.getGridGeometry();
+            if (cv == null) {
+                GridGeometry gg = domain;
+                if (gg == null) {
+                    gg = resource.getGridGeometry();
                 }
-                if (domain != null && domain.getDimension() > BIDIMENSIONAL) {
-                    domain = slice(domain).build();
+                if (gg != null && gg.getDimension() > BIDIMENSIONAL) {
+                    gg = slice(gg).build();
                 }
-                /*
-                 * TODO: We restrict loading to a two-dimensional slice for 
now.
-                 * Future version will need to give user control over slices.
-                 */
-                coverage = resource.read(domain, range);                    // 
May be long to execute.
-                coverage = coverage.forConvertedValues(converted);
+                cv = resource.read(gg, range);
             }
-            if (task.isCancelled()) {
+            coverage = cv = cv.forConvertedValues(converted);
+            if (!render || task.isCancelled()) {
                 return null;
             }
-            GridExtent se = sliceExtent;
-            if (se == null) {
-                final GridGeometry cd = coverage.getGridGeometry();
-                if (cd != null && cd.getDimension() > BIDIMENSIONAL) {      // 
Should never be null but we are paranoiac.
-                    se = slice(cd).getIntersection();
+            GridExtent ex = sliceExtent;
+            if (ex == null) {
+                final GridGeometry gg = cv.getGridGeometry();
+                if (gg != null && gg.getDimension() > BIDIMENSIONAL) {      // 
Should never be null but we are paranoiac.
+                    ex = slice(gg).getIntersection();
                 }
             }
-            return coverage.render(se);
+            return cv.render(ex);
         } finally {
             LogHandler.loadingStop(id);
         }
@@ -315,13 +323,13 @@ public class ImageRequest {
     /**
      * Configures the given status bar with the geometry of the grid coverage 
we have just read.
      * This method is invoked in JavaFX thread after {@link 
GridView#setImage(ImageRequest)}
-     * successfully loaded in background thread a new image.
+     * loaded in background thread a new image, successfully or not.
      */
     final void configure(final StatusBar bar) {
         final Long id = LogHandler.loadingStart(resource);
         try {
             final GridCoverage cv = coverage;
-            final GridExtent request = sliceExtent;
+            final GridExtent ex = sliceExtent;
             bar.applyCanvasGeometry(cv != null ? cv.getGridGeometry() : null);
             /*
              * By `GridCoverage.render(GridExtent)` contract, the 
`RenderedImage` pixel coordinates are relative
@@ -330,10 +338,10 @@ public class ImageRequest {
              * modify `StatusBar.localToObjectiveCRS` because we do not 
associate it to a `MapCanvas`, so it will
              * not be overwritten by gesture events (zoom, pan, etc).
              */
-            if (request != null) {
-                final double[] origin = new double[request.getDimension()];
+            if (ex != null) {
+                final double[] origin = new double[ex.getDimension()];
                 for (int i=0; i<origin.length; i++) {
-                    origin[i] = request.getLow(i);
+                    origin[i] = ex.getLow(i);
                 }
                 bar.localToObjectiveCRS.set(MathTransforms.concatenate(
                         MathTransforms.translation(origin), 
bar.localToObjectiveCRS.get()));
@@ -342,4 +350,19 @@ public class ImageRequest {
             LogHandler.loadingStop(id);
         }
     }
+
+    /**
+     * Reports an exception in a dialog box. This is a convenience method for
+     * {@link javafx.concurrent.Task#succeeded()} implementations.
+     *
+     * @param  owner      control in the window which will own the dialog, or 
{@code null} if unknown.
+     * @param  exception  the error that occurred.
+     */
+    final void reportError(final Node owner, final Throwable exception) {
+        if (resource instanceof StoreListeners) {
+            ExceptionReporter.canNotReadFile(owner, ((StoreListeners) 
resource).getSourceName(), exception);
+        } else {
+            ExceptionReporter.canNotUseResource(owner, exception);
+        }
+    }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/InterpolationConverter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/InterpolationConverter.java
new file mode 100644
index 0000000..5fd0f1e
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/InterpolationConverter.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.gui.coverage;
+
+import java.util.Locale;
+import java.util.Objects;
+import javafx.scene.control.ChoiceBox;
+import javafx.util.StringConverter;
+import org.apache.sis.image.Interpolation;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * Gives a localized {@link String} instance for a given {@link Interpolation} 
and conversely.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.1
+ * @module
+ */
+final class InterpolationConverter extends StringConverter<Interpolation> {
+    /**
+     * Creates the controls for choosing an interpolation method on the given 
canvas.
+     *
+     * @param  view  the canvas for which to create a button for selecting the 
interpolation method.
+     * @return a button for applying an interpolation method on the given view.
+     */
+    static ChoiceBox<Interpolation> button(final CoverageCanvas view) {
+        final ChoiceBox<Interpolation> b = new ChoiceBox<>();
+        b.setConverter(new InterpolationConverter(view.getLocale()));
+        b.getItems().setAll(INTERPOLATIONS);
+        b.getSelectionModel().select(view.getInterpolation());
+        
view.interpolationProperty.bind(b.getSelectionModel().selectedItemProperty());
+        return b;
+    }
+
+    /**
+     * The interpolation supported by this converter.
+     */
+    private static final Interpolation[] INTERPOLATIONS = {
+        Interpolation.NEAREST, Interpolation.BILINEAR, Interpolation.LANCZOS
+    };
+
+    /**
+     * Keys of localized names for each {@link #INTERPOLATIONS} element.
+     */
+    private static final short[] VOCABULARIES = {
+        Vocabulary.Keys.NearestNeighbor, Vocabulary.Keys.Bilinear, 0
+    };
+
+    /**
+     * The locale to use for string representation.
+     */
+    private final Locale locale;
+
+    /**
+     * Creates a new converter for the given locale.
+     */
+    private InterpolationConverter(final Locale locale) {
+        this.locale = locale;
+    }
+
+    /**
+     * Returns a string representation of the given item.
+     */
+    @Override
+    public String toString(final Interpolation item) {
+        for (int i=0; i<INTERPOLATIONS.length; i++) {
+            if (INTERPOLATIONS[i].equals(item)) {
+                final short key = VOCABULARIES[i];
+                if (key != 0) {
+                    return Vocabulary.getResources(locale).getString(key);
+                } else if (item == Interpolation.LANCZOS) {
+                    return "Lanczos";
+                }
+            }
+        }
+        return Objects.toString(item);
+    }
+
+    /**
+     * Returns the interpolation for the given text.
+     */
+    @Override
+    public Interpolation fromString(final String text) {
+        final Vocabulary vocabulary = Vocabulary.getResources(locale);
+        for (int i=0; i<VOCABULARIES.length; i++) {
+            final short key = VOCABULARIES[i];
+            final Interpolation item = INTERPOLATIONS[i];
+            if ((key != 0 && vocabulary.getString(key).equalsIgnoreCase(text))
+                                    || item.toString().equalsIgnoreCase(text))
+            {
+                return item;
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/PropertyPaneCreator.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/PropertyPaneCreator.java
new file mode 100644
index 0000000..066dd09
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/PropertyPaneCreator.java
@@ -0,0 +1,65 @@
+/*
+ * 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 javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.TitledPane;
+
+
+/**
+ * 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.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.1
+ * @module
+ */
+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} when {@link 
TitledPane#expandedProperty()} changed.
+     */
+    @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());
+        }
+    }
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java
index 398bb25..795eaa4 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.gui.coverage;
 
-import java.lang.ref.Reference;
 import javafx.geometry.Insets;
 import javafx.scene.control.Control;
 import javafx.scene.control.Label;
@@ -31,8 +30,11 @@ import org.apache.sis.util.resources.IndexedResourceBundle;
 
 
 /**
- * A {@link GridView} or {@link CoverageCanvas} together with the controls
- * to show in a {@link CoverageExplorer}.
+ * A {@link GridView} or {@link CoverageCanvas} together with the controls to 
show in a {@link CoverageExplorer}.
+ * When the image or coverage is updated in a view, the {@link 
#coverageChanged(Resource, GridCoverage)} method
+ * is invoked, which will in turn update the {@link CoverageExplorer} 
properties. Coverage changes are applied
+ * on the view then propagated to {@code CoverageExplorer} rather than the 
opposite direction because loading
+ * mechanisms are implemented in the view (different views may load a 
different amount of data).
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
@@ -63,9 +65,25 @@ abstract class ViewAndControls {
     Toggle selector;
 
     /**
+     * The widget which contain this view. This is the widget to inform when 
the coverage changed.
+     * Subclasses should define the following method:
+     *
+     * {@preformat java
+     *     private void coverageChanged(final Resource source, final 
GridCoverage data) {
+     *         // Update subclass-specific controls here, before to forward to 
explorer.
+     *         owner.coverageChanged(source, data);
+     *     }
+     * }
+     */
+    protected final CoverageExplorer owner;
+
+    /**
      * Creates a new view-control pair.
+     *
+     * @param  owner  the widget which create this view. Can not be null.
      */
-    ViewAndControls() {
+    ViewAndControls(final CoverageExplorer owner) {
+        this.owner = owner;
     }
 
     /**
@@ -107,7 +125,7 @@ abstract class ViewAndControls {
     /**
      * Returns the font to assign to the label of a group of control.
      */
-    static Font fontOfGroup() {
+    private static Font fontOfGroup() {
         return Font.font(null, FontWeight.BOLD, -1);
     }
 
@@ -124,13 +142,10 @@ abstract class ViewAndControls {
     abstract Control controls();
 
     /**
-     * Invoked in JavaFX thread after {@link 
CoverageExplorer#setCoverage(ImageRequest)} completed.
-     * Implementation should update the GUI with new information available, in 
particular
-     * the coordinate reference system and the list of sample dimensions.
+     * Sets the view content to the given resource, coverage or image.
+     * This method may start a background thread.
      *
-     * @param  data        the new coverage, or {@code null} if none.
-     * @param  originator  the resource from which the data has been read, or 
{@code null} if unknown.
-     *                     This is used for determining a target window for 
logging records.
+     * @param  request  the resource, coverage or image to set, or {@code 
null} for clearing the view.
      */
-    abstract void coverageChanged(GridCoverage data, Reference<Resource> 
originator);
+    abstract void load(ImageRequest request);
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
index 1a42b35..a6b9fbc 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -132,20 +132,16 @@ public class ResourceExplorer extends WindowManager {
      * The {@link #features} and {@link #coverage} data will be set only if a 
data tab is visible,
      * because the data loading may be costly.
      *
-     * @see #isDataTabSet
-     * @see #isDataTabSelected()
-     * @see #updateDataTab(Resource, boolean)
+     * @see #updateDataTab(Resource)
      */
     private final Tab viewTab, tableTab;
 
     /**
-     * Whether the setting of new values in {@link #viewTab} or {@link 
#tableTab} has been done.
-     * The new values are set only if a data tab is visible, and otherwise are 
delayed until one
-     * of data tab become visible.
+     * Whether one of the "view" or "table" tab is shown. They are the tabs 
requiring data loading.
      *
-     * @see #updateDataTab(Resource, boolean)
+     * @see #getCoverageView()
      */
-    private boolean isDataTabSet;
+    private final BooleanBinding dataShown;
 
     /**
      * Whether one of the standard metadata tab (either "summary" or 
"metadata") is selected.
@@ -177,10 +173,9 @@ public class ResourceExplorer extends WindowManager {
         final Tab summaryTab = new 
Tab(vocabulary.getString(Vocabulary.Keys.Summary),  metadata.getView());
         /*
          * "Visual" tab showing the raster data as an image.
-         *
-         * TODO: add contextual menu for creating a window showing directly 
the visual.
          */
         viewTab = new Tab(vocabulary.getString(Vocabulary.Keys.Visual));
+        viewTab.setContextMenu(new 
ContextMenu(SelectedData.setTabularView(createNewWindowMenu())));
         /*
          * "Data" tab showing raster data as a table.
          */
@@ -219,12 +214,16 @@ public class ResourceExplorer extends WindowManager {
         SplitPane.setResizableWithParent(resources, Boolean.FALSE);
         SplitPane.setResizableWithParent(tabs, Boolean.TRUE);
         /*
-         * Register listeners last, for making sure we don't have undesired 
event.
+         * Register listeners last, for making sure we do not have undesired 
events.
          * Those listeners trig loading of various objects (data, standard 
metadata,
          * native metadata) when the corresponding tab become visible.
          */
-        viewTab .selectedProperty().addListener((p,o,n) -> dataTabShown(n, 
true));
-        tableTab.selectedProperty().addListener((p,o,n) -> dataTabShown(n, 
false));
+        dataShown = viewTab.selectedProperty().or(tableTab.selectedProperty());
+        dataShown.addListener((p,o,n) -> {
+            if (Boolean.FALSE.equals(o) && Boolean.TRUE.equals(n)) {
+                updateDataTabWithDefault(getSelectedResource());
+            }
+        });
         metadataShown = 
summaryTab.selectedProperty().or(metadataTab.selectedProperty());
         metadataShown.addListener((p,o,n) -> {
             if (Boolean.FALSE.equals(o) && Boolean.TRUE.equals(n)) {
@@ -357,15 +356,11 @@ public class ResourceExplorer extends WindowManager {
          */
         selectedResource.set(resource);
         metadata.setMetadata(metadataShown.get() ? resource : null);
-        isDataTabSet = viewTab.isSelected() || tableTab.isSelected();
-        updateDataTab(isDataTabSet ? resource : null, true);
-        if (!isDataTabSet) {
-            setNewWindowDisabled(!(resource instanceof GridCoverageResource || 
resource instanceof FeatureSet));
-        }
+        updateDataTabWithDefault(dataShown.get() ? resource : null);
         /*
-         * Update the label is disabled state of the native metadata tab. We 
do not have a reliable way
+         * Update the label and disabled state of the native metadata tab. We 
do not have a reliable way
          * to know if metadata are present without trying to fetch them, so 
current implementation only
-         * checks if the data store implementation override the 
`getNativeMetadata()` method.
+         * checks if the data store implementation overrides the 
`getNativeMetadata()` method.
          */
         String  label    = null;
         boolean disabled = true;
@@ -378,7 +373,7 @@ public class ResourceExplorer extends WindowManager {
             try {
                 disabled = 
resource.getClass().getMethod("getNativeMetadata").getDeclaringClass() == 
DataStore.class;
             } catch (NoSuchMethodException e) {
-                // Should never happen.
+                warning("onResourceSelected", resource, e);         // Should 
never happen.
             }
         }
         nativeMetadataTab.setText(Objects.toString(label, 
defaultNativeTabLabel));
@@ -392,8 +387,9 @@ public class ResourceExplorer extends WindowManager {
 
     /**
      * Loads native metadata in a background thread and shows them in the 
"native metadata" tab.
+     * This method is invoked when the tab become visible, or when a new 
resource is loaded.
      */
-    private final void loadNativeMetadata() {
+    private void loadNativeMetadata() {
         final Resource resource = getSelectedResource();
         if (resource instanceof DataStore) {
             final DataStore store = (DataStore) resource;
@@ -419,41 +415,54 @@ public class ResourceExplorer extends WindowManager {
     }
 
     /**
-     * Assigns the given resource into the {@link #viewTab} and {@link 
#tableTab}. Should be invoked only
-     * if a data tab is visible because data loading may be costly. It is 
caller responsibility to invoke
-     * {@link #setNewWindowDisabled(boolean)} after this method.
+     * Returns the enumeration value that describe the kind of content to show 
in {@link CoverageExplorer}.
+     * The type depends on which tab is visible. If no coverage data tab is 
visible, then returns null.
      *
-     * <p>The {@link #isDataTabSet} flag should be set before to invoke this 
method. If {@code true}, then
-     * the given resource is the final content and window menus will be 
updated accordingly by this method.
-     * If {@code false}, then the given resource is temporarily null and 
window menus should be updated by
-     * the caller instead of this method.</p>
+     * @see #dataShown
+     */
+    private CoverageExplorer.View getCoverageView() {
+        if  (viewTab.isSelected()) return CoverageExplorer.View.IMAGE;
+        if (tableTab.isSelected()) return CoverageExplorer.View.TABLE;
+        return null;
+    }
+
+    /**
+     * Assigns the given resource into the {@link #viewTab} or {@link 
#tableTab}, depending which one is visible.
+     * Shall be invoked with a non-null resource only if a data tab is visible 
because data loading may be costly.
      *
      * @param  resource  the resource to set, or {@code null} if none.
-     * @param  fallback  whether to allow the search for a default component 
to show
-     *                   if the given resource is an aggregate.
-     */
-    private void updateDataTab(final Resource resource, boolean fallback) {
-        Region       image = null;
-        Region       table = null;
-        FeatureSet   data  = null;
-        ImageRequest grid  = null;
+     * @return {@code true} if the resource has been recognized.
+     *
+     * @see #dataShown
+     * @see #updateDataTabWithDefault(Resource)
+     */
+    private boolean updateDataTab(final Resource resource) {
+        Region        image = null;
+        Region        table = null;
+        FeatureSet    data  = null;
+        ImageRequest  grid  = null;
+        Region controlPanel = null;
         CoverageExplorer.View type = null;
         if (resource instanceof GridCoverageResource) {
+            type = getCoverageView();       // A null value here would be a 
violation of method contract.
             if (coverage == null) {
-                coverage = new CoverageExplorer();
+                coverage = new CoverageExplorer(type);
+            } else {
+                coverage.setViewType(type);
+            }
+            final Region view = coverage.getDataView(type);
+            switch (type) {
+                case IMAGE: image = view; break;
+                case TABLE: table = view; break;
             }
-            grid  = new ImageRequest((GridCoverageResource) resource, null, 
null);
-            image = coverage.getDataView(CoverageExplorer.View.IMAGE);
-            table = coverage.getDataView(CoverageExplorer.View.TABLE);
-            type  = viewTab.isSelected() ? CoverageExplorer.View.IMAGE : 
CoverageExplorer.View.TABLE;
-            fallback = false;
+            grid = new ImageRequest((GridCoverageResource) resource, null, 
null);
+            controlPanel = coverage.getControls(type);
         } else if (resource instanceof FeatureSet) {
             data = (FeatureSet) resource;
             if (features == null) {
                 features = new FeatureTable();
             }
             table = features;
-            fallback = false;
         }
         /*
          * At least one of `grid` or `data` will be null. Invoking the 
following
@@ -463,44 +472,19 @@ public class ResourceExplorer extends WindowManager {
         if (features != null) features.setFeatures(data);
         if (image    != null) viewTab .setContent(image);
         if (table    != null) tableTab.setContent(table);
-        if (isDataTabSet) {
-            setNewWindowDisabled(image == null && table == null);
-            updateControls(type);
-        }
-        if (fallback) {
-            defaultIfNotViewable(resource);
-        }
-    }
-
-    /**
-     * Invoked when a data tab become selected or unselected.
-     * This method sets the current resource in the {@link #viewTab}
-     * or {@link #tableTab} if it has not been already set.
-     *
-     * @param  selected  whether the tab became the selected one.
-     * @param  visual    {@code true} for visual, or {@code false} for tabular 
data.
-     */
-    private void dataTabShown(final Boolean selected, final boolean visual) {
-        CoverageExplorer.View type = null;
-        if (selected) {
-            if (!isDataTabSet) {
-                isDataTabSet = true;                    // Must be set before 
to invoke `updateDataTab(…)`.
-                updateDataTab(getSelectedResource(), true);
-            }
-            if (coverage != null) {                     // May still be null 
if the selected resource is not a coverage.
-                type = visual ? CoverageExplorer.View.IMAGE : 
CoverageExplorer.View.TABLE;
-            }
-        }
-        updateControls(type);
+        final boolean isEmpty = (image == null & table == null);
+        setNewWindowDisabled(isEmpty);
+        updateControls(controlPanel);
+        return !isEmpty | (resource == null);
     }
 
     /**
      * Adds or removes controls for the given view.
+     * This method is invoked when the visible tab changed.
      *
-     * @param  type  the view for which to provide controls, or {@code null} 
if none.
+     * @param  controlPanel  the controls for the currently selected tab, or 
{@code null} if none.
      */
-    private void updateControls(final CoverageExplorer.View type) {
-        final Region controlPanel = (type != null) ? 
coverage.getControls(type) : null;
+    private void updateControls(final Region controlPanel) {
         final ObservableList<Node> items = controls.getItems();
         if (items.size() >= 2) {
             if (controlPanel != null) {
@@ -519,6 +503,7 @@ public class ResourceExplorer extends WindowManager {
 
     /**
      * Returns the set of currently selected data, or {@code null} if none.
+     * This is invoked when the user selects the "New window" menu item.
      */
     @Override
     final SelectedData getSelectedData() {
@@ -540,9 +525,8 @@ public class ResourceExplorer extends WindowManager {
              * We do that even if the feature table is not currently visible. 
This will not cause
              * useless data loading since they share the same `FeatureLoader`.
              */
-            if (features == null || !isDataTabSet) {
-                isDataTabSet = true;                    // Must be set before 
to invoke `updateDataTab(…)`.
-                updateDataTab(resource, true);          // For forcing 
creation of FeatureTable.
+            if (features == null) {
+                updateDataTab(resource);                // For forcing 
creation of FeatureTable.
             }
             table = features;
         } else {
@@ -576,14 +560,19 @@ public class ResourceExplorer extends WindowManager {
     }
 
     /**
-     * If the given resource is not one of the resource that {@link 
#updateDataTab(Resource, boolean)}
-     * can handle, searches in a background thread for a default resource to 
show. The purpose of this
-     * method is to make navigation easier by allowing users to click on the 
root node of a resource,
+     * If the given resource is not one of the resource that {@link 
#updateDataTab(Resource)} can handle,
+     * searches in a background thread for a default resource to show. The 
purpose of this method is to
+     * make navigation easier by allowing users to click on the root node of a 
resource,
      * without requerying them to expand the tree node before to select a 
resource.
      *
      * @param  resource  the selected resource.
+     *
+     * @see #updateDataTab(Resource)
      */
-    private void defaultIfNotViewable(final Resource resource) {
+    private void updateDataTabWithDefault(final Resource resource) {
+        if (updateDataTab(resource)) {
+            return;
+        }
         if (resource instanceof Aggregate && !(resource instanceof DataSet)) {
             BackgroundThreads.execute(new Task<Resource>() {
                 /** Invoked in background thread for fetching the first 
resource. */
@@ -604,23 +593,33 @@ public class ResourceExplorer extends WindowManager {
                 /** Invoked in JavaFX thread for showing the resource. */
                 @Override protected void succeeded() {
                     if (getSelectedResource() == resource) {
-                        updateDataTab(getValue(), false);
+                        updateDataTab(getValue());
                     }
                 }
 
                 /** Invoked in JavaFX thread if children can not be loaded. */
                 @Override protected void failed() {
-                    final ObservableList<LogRecord> records = 
LogHandler.getRecords(resource);
-                    if (records != null) {
-                        final Throwable e = getException();
-                        final LogRecord record = new LogRecord(Level.WARNING, 
e.getLocalizedMessage());
-                        
record.setSourceClassName(ResourceExplorer.class.getName());
-                        record.setSourceMethodName("defaultIfNotViewable");
-                        record.setThrown(e);
-                        records.add(record);
-                    }
+                    warning("updateDataTabWithDefault", resource, 
getException());
                 }
             });
         }
     }
+
+    /**
+     * Adds a warning to the logger associated to the resource.
+     *
+     * @param caller    the method to declare as the source of the warning.
+     * @param resource  the resource for which an exception occurred.
+     * @param error     the exception to log.
+     */
+    private static void warning(final String caller, final Resource resource, 
final Throwable error) {
+        final ObservableList<LogRecord> records = 
LogHandler.getRecords(resource);
+        if (records != null) {
+            final LogRecord record = new LogRecord(Level.WARNING, 
error.getLocalizedMessage());
+            record.setSourceClassName(ResourceExplorer.class.getName());
+            record.setSourceMethodName(caller);
+            record.setThrown(error);
+            records.add(record);
+        }
+    }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java
index aca933d..13c9c06 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java
@@ -109,13 +109,8 @@ final class SelectedData {
                     }
                 }
             }
-            final CoverageExplorer ce = new CoverageExplorer();
+            final CoverageExplorer ce = new CoverageExplorer(view);
             ce.setCoverage(coverage);
-            /*
-             * TODO: following line is disabled for now because it causes
-             *       the vertical scroll bar of `GridView` to disappear.
-             */
-//          ce.setViewType(view);
             return ce.getView();
         }
     }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
index 4cf5681..47fc08b 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
@@ -84,7 +84,7 @@ abstract class WindowManager extends Widget {
      * Creates a new manager of windows.
      */
     WindowManager() {
-        newWindowMenus     = new ArrayList<>(2);
+        newWindowMenus     = new ArrayList<>(3);
         hasWindowsProperty = new WindowsProperty();
     }
 
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
index b17445e..7eb7382 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
@@ -307,7 +307,6 @@ public class MetadataSummary extends Widget {
                                     final Metadata oldValue, final Metadata 
metadata)
     {
         final MetadataSummary s = (MetadataSummary) ((SimpleObjectProperty<?>) 
property).getBean();
-        final Getter getter = s.getter;
         s.getter = null;                // In case this method is invoked 
before `Getter` completed.
         s.error  = null;
         if (metadata != oldValue) {

Reply via email to