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 c112a871b485aa20f147b327d13c56b43a7051f2
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon May 30 17:16:12 2022 +0200

    Initial version of a `CoverangeCanvas` capable to navigate in dimensions 
over 2 (using sliders).
    It required a change in the ways controls are managed, e.g. with 
`StatusBar` now managed by `ViewAndControls`.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    | 155 ++++++++++------
 .../apache/sis/gui/coverage/CoverageControls.java  |  22 ++-
 .../org/apache/sis/gui/coverage/GridControls.java  |   7 +-
 .../apache/sis/gui/coverage/GridSliceSelector.java |  28 ++-
 .../java/org/apache/sis/gui/coverage/GridView.java |  87 ++++++---
 .../org/apache/sis/gui/coverage/GridViewSkin.java  |   4 +-
 .../org/apache/sis/gui/coverage/ImageRequest.java  | 196 +++++----------------
 .../gui/coverage/MultiResolutionImageLoader.java   |  74 +-------
 .../apache/sis/gui/coverage/ViewAndControls.java   |  44 +++--
 .../java/org/apache/sis/gui/map/StatusBar.java     | 173 +++++++++++++-----
 .../apache/sis/internal/gui/BackgroundThreads.java |  24 ++-
 .../apache/sis/internal/gui/ExceptionReporter.java |  26 ++-
 .../sis/internal/gui/OptionalDataDownloader.java   |   2 +-
 .../org/apache/sis/internal/gui/package-info.java  |   2 +-
 .../apache/sis/gui/coverage/CoverageCanvasApp.java |   4 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |  18 +-
 .../coverage/MultiResolutionCoverageLoader.java    |  24 +--
 .../sis/internal/map/coverage/RenderingData.java   | 117 ++++++++----
 .../sis/internal/map/coverage/package-info.java    |   2 +-
 .../java/org/apache/sis/util/ArgumentChecks.java   |  29 ++-
 20 files changed, 601 insertions(+), 437 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 67bd1a3b66..57eb233337 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
@@ -47,6 +47,7 @@ import javax.measure.Quantity;
 import javax.measure.quantity.Length;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.coverage.SampleDimension;
@@ -57,16 +58,15 @@ import 
org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.CommonCRS;
-import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.Interpolation;
 import org.apache.sis.coverage.Category;
+import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.gui.map.MapCanvasAWT;
-import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.portrayal.RenderException;
 import org.apache.sis.internal.map.coverage.RenderingWorkaround;
 import org.apache.sis.internal.coverage.j2d.TileErrorHandler;
@@ -79,6 +79,7 @@ import org.apache.sis.internal.system.Modules;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.measure.Units;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Debug;
 
 import static java.util.logging.Logger.getLogger;
@@ -161,6 +162,11 @@ public class CoverageCanvas extends MapCanvasAWT {
      */
     private boolean isCoverageAdjusting;
 
+    /**
+     * Whether at least one of {@link #coverageProperty} or {@link 
#resourceProperty} has a non-null value.
+     */
+    private boolean hasCoverageOrResource;
+
     /**
      * A subspace of the grid coverage extent where all dimensions except two 
have a size of 1 cell.
      * May be {@code null} if the grid coverage has only two dimensions with a 
size greater than 1 cell.
@@ -181,8 +187,8 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * The {@code RenderedImage} to draw together with transform from pixel 
coordinates to display coordinates.
-     * Shall never be {@code null} but may be {@link 
StyledRenderingData#isEmpty() empty}. This instance shall
-     * be read and modified in JavaFX thread only and cloned if those data are 
needed by a background thread.
+     * Shall never be {@code null} but may be empty. This instance shall be 
read and modified in JavaFX thread
+     * only and cloned if those data are needed by a background thread.
      *
      * @see Worker
      */
@@ -213,10 +219,12 @@ public class CoverageCanvas extends MapCanvasAWT {
     private ImagePropertyExplorer propertyExplorer;
 
     /**
-     * The status bar associated to this {@code MapCanvas}.
-     * This is non-null only if this {@link CoverageCanvas} is used together 
with {@link CoverageControls}.
+     * If this canvas is associated with controls, the controls. Otherwise 
{@code null}.
+     * This is used only for notifications; a future version may use a more 
generic listener.
+     *
+     * @see CoverageControls#notifyDataChanged(GridCoverageResource, 
GridCoverage)
      */
-    StatusBar statusBar;
+    private final CoverageControls controls;
 
     /**
      * If errors occurred during tile computations, details about the error. 
Otherwise {@code null}.
@@ -239,16 +247,18 @@ public class CoverageCanvas extends MapCanvasAWT {
      * Creates a new two-dimensional canvas for {@link RenderedImage}.
      */
     public CoverageCanvas() {
-        this(Locale.getDefault());
+        this(null, Locale.getDefault());
     }
 
     /**
      * Creates a new two-dimensional canvas using the given locale.
      *
-     * @param  locale  the locale to use for labels and some messages, or 
{@code null} for default.
+     * @param  controls  the controls of this canvas, or {@code null} if none.
+     * @param  locale    the locale to use for labels and some messages, or 
{@code null} for default.
      */
-    CoverageCanvas(final Locale locale) {
+    CoverageCanvas(final CoverageControls controls, final Locale locale) {
         super(locale);
+        this.controls         = controls;
         data                  = new StyledRenderingData((report) -> 
errorReport = report.getDescription());
         derivedImages         = new EnumMap<>(Stretching.class);
         resourceProperty      = new SimpleObjectProperty<>(this, "resource");
@@ -367,6 +377,8 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * Sets a subspace of the grid coverage extent where all dimensions except 
two have a size of 1 cell.
+     * Note that values set on this property may be overwritten at any time by 
user interactions if this
+     * {@code CoverageCanvas} is associated with a {@link GridSliceSelector}.
      *
      * @param  sliceExtent  subspace of the grid coverage extent where all 
dimensions except two have a size of 1 cell.
      *
@@ -485,8 +497,8 @@ public class CoverageCanvas extends MapCanvasAWT {
         final GridExtent sliceExtent;
         if (request != null) {
             resource    = request.resource;
-            coverage    = request.getCoverage().orElse(null);
-            sliceExtent = request.getSliceExtent().orElse(null);
+            coverage    = request.coverage;
+            sliceExtent = request.slice;
         } else {
             resource    = null;
             coverage    = null;
@@ -519,6 +531,7 @@ public class CoverageCanvas extends MapCanvasAWT {
     private void onPropertySpecified(final GridCoverageResource resource, 
final GridCoverage coverage,
                                      final ObjectProperty<?> toClear)
     {
+        hasCoverageOrResource = (resource != null || coverage != null);
         if (isCoverageAdjusting) {
             return;
         }
@@ -531,15 +544,17 @@ public class CoverageCanvas extends MapCanvasAWT {
         if (resource == null && coverage == null) {
             runAfterRendering(this::clear);
         } else {
-            BackgroundThreads.execute(new Task<Envelope>() {
-                /** The coverage geometry reduced to two dimensions. */
-                private GridGeometry domain;
-
+            BackgroundThreads.execute(new Task<GridGeometry>() {
                 /** Information about all bands. */
                 private List<SampleDimension> ranges;
 
-                /** Fetch coverage domain, range and geospatial envelope. */
-                @Override protected Envelope call() throws Exception {
+                /**
+                 * Fetches coverage domain and range. In some {@link 
GridCoverageResource} implementations,
+                 * fetching the grid geometry is a costly operation. So we do 
it in a background thread and
+                 * invoke {@code setNewSource(…)} later with the result. No 
rendering happen here.
+                 */
+                @Override protected GridGeometry call() throws Exception {
+                    GridGeometry domain;
                     final Long id = LogHandler.loadingStart(resource);
                     try {
                         if (coverage != null) {
@@ -549,26 +564,33 @@ public class CoverageCanvas extends MapCanvasAWT {
                             domain = resource.getGridGeometry();
                             ranges = resource.getSampleDimensions();
                         }
-                        domain = MultiResolutionImageLoader.slice(domain);
-                        if (domain != null) {
-                            if (domain.isDefined(GridGeometry.ENVELOPE)) {
-                                return domain.getEnvelope();
-                            }
-                            if (domain.isDefined(GridGeometry.EXTENT)) {
-                                final GeneralEnvelope ge = 
domain.getExtent().toEnvelope(MathTransforms.identity(BIDIMENSIONAL));
-                                
ge.setCoordinateReferenceSystem(CommonCRS.Engineering.DISPLAY.crs());
-                                return ge;
-                            }
+                        /*
+                         * The domain should never be null and should always 
be complete (including envelope).
+                         * Nevertheless we try to be safe, since 
`setNewSource(…)` wants a complete geometry.
+                         * So if the envelope is missing but the extent is 
present, then the missing part was
+                         * the "grid to CRS" transform. We use an identity 
transform with a "display CRS".
+                         */
+                        if (domain != null && 
!domain.isDefined(GridGeometry.ENVELOPE) && 
domain.isDefined(GridGeometry.EXTENT)) {
+                            final GridExtent extent = domain.getExtent();
+                            final int dimension = extent.getDimension();
+                            domain = new GridGeometry(extent, 
PixelInCell.CELL_CORNER, MathTransforms.identity(dimension),
+                                            (dimension == BIDIMENSIONAL) ? 
CommonCRS.Engineering.DISPLAY.crs() : null);
                         }
-                        return null;
                     } finally {
                         LogHandler.loadingStop(id);
                     }
+                    return domain;
                 }
 
-                /** Invoked in JavaFX thread for setting the grid geometry we 
just fetched. */
+                /**
+                 * Invoked in JavaFX thread for setting the grid geometry we 
just fetched.
+                 * This method requests a repaint, which will occur in another 
thread.
+                 */
                 @Override protected void succeeded() {
-                    runAfterRendering(() -> setNewSource(getValue(), domain, 
ranges));
+                    runAfterRendering(() -> {
+                        setNewSource(getValue(), ranges);
+                        requestRepaint();                   // Cause `Worker` 
class to be executed.
+                    });
                 }
 
                 /**
@@ -587,17 +609,16 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * Invoked when a new resource or coverage has been specified.
-     * The call to this method is followed by a repaint event,
-     * which will cause the image to be loaded and resampled in a background 
thread.
+     * Caller should invoke {@link #requestRepaint()} after this method
+     * for loading and resampling the image in a background thread.
      *
      * <p>All arguments can be {@code null} for clearing the canvas.
      * This method is invoked in JavaFX thread.</p>
      *
-     * @param  bounds  geospatial bounds, or {@code null} if none or unknown.
-     * @param  domain  the two-dimensional grid geometry, or {@code null} if 
there is no data.
+     * @param  domain  the multi-dimensional grid geometry, or {@code null} if 
there is no data.
      * @param  ranges  descriptions of bands, or {@code null} if there is no 
data.
      */
-    private void setNewSource(final Envelope bounds, final GridGeometry 
domain, final List<SampleDimension> ranges) {
+    private void setNewSource(GridGeometry domain, final List<SampleDimension> 
ranges) {
         if (TRACE) {
             trace("setNewSource(…): the new domain of data is:%n\t%s", domain);
         }
@@ -606,9 +627,39 @@ public class CoverageCanvas extends MapCanvasAWT {
         resampledImage = null;
         derivedImages.clear();
         data.clear();
-        data.setCoverageSpace(domain, ranges);
+        /*
+         * Configure the `GridSliceSelector`, which will compute a new slice 
extent as a side effect.
+         * It will overwrite the previous value of `sliceExtent` property in 
this class, which needs
+         * to be done before to start the `Worker` process in a background 
thread.
+         */
+        int[] xyDimensions;
+        if (controls != null) try {
+            isCoverageAdjusting = true;
+            setSliceExtent(controls.setGeometry(domain));
+            xyDimensions = controls.sliceSelector.getXYDimensions();
+        } finally {
+            isCoverageAdjusting = false;
+        } else {
+            xyDimensions = ArraysExt.range(0, BIDIMENSIONAL);
+            final GridExtent extent = getSliceExtent();
+            if (extent != null) try {
+                xyDimensions = extent.getSubspaceDimensions(BIDIMENSIONAL);
+            } catch (SubspaceNotSpecifiedException e) {
+                unexpectedException(e);                     // We can continue 
with dimensions {0,1}.
+            }
+        }
+        /*
+         * Notify the `RenderingData` and `MapCanvas`. All information below 
must be two-dimensional.
+         */
+        Envelope bounds = null;
+        if (domain != null) {
+            domain = domain.reduce(xyDimensions);
+            if (domain.isDefined(GridGeometry.ENVELOPE)) {
+                bounds = domain.getEnvelope();
+            }
+        }
+        data.setImageSpace(domain, ranges, xyDimensions);
         setObjectiveBounds(bounds);
-        requestRepaint();                       // Cause `Worker` class to be 
executed.
     }
 
     /**
@@ -642,7 +693,7 @@ public class CoverageCanvas extends MapCanvasAWT {
      */
     @Override
     protected Renderer createRenderer() {
-        return (data.getSourceImage() != null || getResource() != null) ? new 
Worker(this) : null;
+        return hasCoverageOrResource ? new Worker(this) : null;
     }
 
     /**
@@ -668,20 +719,22 @@ public class CoverageCanvas extends MapCanvasAWT {
 
         /**
          * The coverage specified by user or the coverage loaded from the 
{@linkplain #resource}.
-         * May be {@code null} if coverages are loaded from resource but did 
not changed since last rendering.
+         * Should never be {@code null} after successful execution of {@link 
#render()}.
          */
         private GridCoverage coverage;
 
         /**
          * Whether the value of {@link #coverage} changed since the last 
rendering.
-         * It may happen if {@link #resource} is non-null, contains pyramided 
data
-         * and the pyramid level used by this rendering is different than the 
pyramid
-         * level used in previous rendering.
+         * It may happen if {@link #resource} is non-null, contains pyramided 
data and the pyramid level
+         * used by this rendering is different than the pyramid level used 
during the previous rendering.
+         * Note that a {@code false} value does not mean that {@link 
#sliceExtent} did not changed.
          */
         private boolean coverageChanged;
 
         /**
-         * The two-dimensional slice to display.
+         * The two-dimensional slice to display. May change for the same 
coverage when using
+         * {@link ViewAndControls#sliceSelector} for navigation in dimensions 
other than the
+         * {@value #BIDIMENSIONAL} first dimensions.
          */
         private final GridExtent sliceExtent;
 
@@ -827,10 +880,12 @@ public class CoverageCanvas extends MapCanvasAWT {
                 data.setObjectiveCRS(objectiveCRS);
                 if (resource != null) {
                     data.coverageLoader = 
MultiResolutionImageLoader.getInstance(resource, data.coverageLoader);
-                    coverage = data.ensureCoverageLoaded(objectiveToDisplay, 
objectivePOI);
-                    coverageChanged = (coverage != null);
+                    final GridCoverage loaded = 
data.ensureCoverageLoaded(objectiveToDisplay, objectivePOI);
+                    if (coverageChanged = (loaded != null)) {
+                        coverage = loaded;
+                    }
                 }
-                if (data.ensureImageLoaded(coverage, sliceExtent)) {
+                if (data.ensureImageLoaded(coverage, sliceExtent, 
coverageChanged)) {
                     recoloredImage = null;
                 }
                 /*
@@ -966,7 +1021,7 @@ public class CoverageCanvas extends MapCanvasAWT {
          * Adjust the accuracy of coordinates shown in the status bar.
          * The number of fraction digits depend on the zoom factor.
          */
-        if (statusBar != null) {
+        if (controls != null) {
             final Object value = 
resampledImage.getProperty(PlanarImage.POSITIONAL_ACCURACY_KEY);
             Quantity<Length> accuracy = null;
             if (value instanceof Quantity<?>[]) {
@@ -979,7 +1034,7 @@ public class CoverageCanvas extends MapCanvasAWT {
                     }
                 }
             }
-            statusBar.setLowestAccuracy(accuracy);
+            controls.status.setLowestAccuracy(accuracy);
         }
         /*
          * If error(s) occurred during calls to `RenderedImage.getTile(tx, 
ty)`, reports those errors.
@@ -1081,7 +1136,7 @@ public class CoverageCanvas extends MapCanvasAWT {
         if (TRACE) {
             trace("clear()");
         }
-        setNewSource(null, null, null);
+        setNewSource(null, null);
         super.clear();
     }
 
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 76f630a8ca..8952454ddd 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
@@ -31,7 +31,6 @@ import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.gui.map.MapMenu;
-import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.internal.gui.control.ValueColorMapper;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.gui.Resources;
@@ -77,12 +76,12 @@ final class CoverageControls extends ViewAndControls {
         final Resources  resources  = Resources.forLocale(locale);
         final Vocabulary vocabulary = Vocabulary.getResources(locale);
 
-        view = new CoverageCanvas(locale);
+        view = new CoverageCanvas(this, locale);
         view.setBackground(Color.BLACK);
-        view.statusBar = new StatusBar(owner.referenceSystems, view);
+        status.track(view);
         final MapMenu menu = new MapMenu(view);
         menu.addReferenceSystems(owner.referenceSystems);
-        menu.addCopyOptions(view.statusBar);
+        menu.addCopyOptions(status);
         /*
          * "Display" section with the following controls:
          *    - Current CRS
@@ -149,7 +148,7 @@ final class CoverageControls extends ViewAndControls {
         view.resourceProperty.addListener((p,o,n) -> notifyDataChanged(n, 
null));
         view.coverageProperty.addListener((p,o,n) -> 
notifyDataChanged(view.getResourceIfAdjusting(), n));
         deferred.expandedProperty().addListener(new PropertyPaneCreator(view, 
deferred));
-        setView(view.getView(), view.statusBar);
+        setView(view.getView());
     }
 
     /**
@@ -160,8 +159,7 @@ final class CoverageControls extends ViewAndControls {
      * @param  resource  the new source of coverage, or {@code null} if none.
      * @param  coverage  the new coverage, or {@code null} if none.
      */
-    @Override
-    final void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
+    private void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
         final ObservableList<Category> items = categoryTable.getItems();
         if (coverage == null) {
             items.clear();
@@ -169,7 +167,15 @@ final class CoverageControls extends ViewAndControls {
             final int visibleBand = 0;          // TODO: provide a selector 
for the band to show.
             
items.setAll(coverage.getSampleDimensions().get(visibleBand).getCategories());
         }
-        super.notifyDataChanged(resource, coverage);
+        owner.notifyDataChanged(resource, coverage);
+    }
+
+    /**
+     * Returns the grid coverage shown in the view, or {@code null} if none.
+     */
+    @Override
+    GridCoverage getCoverage() {
+        return view.getCoverage();
     }
 
     /**
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 eac6982f0a..542cb90bcf 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
@@ -58,7 +58,7 @@ final class GridControls extends ViewAndControls {
      */
     GridControls(final CoverageExplorer owner) {
         super(owner);
-        view = new GridView(this, owner.referenceSystems);
+        view = new GridView(this);
         final Vocabulary vocabulary = 
Vocabulary.getResources(owner.getLocale());
         sampleDimensions = new 
BandRangeTable(view.cellFormat).create(vocabulary);
         BandSelectionListener.bind(view.bandProperty, 
sampleDimensions.getSelectionModel());
@@ -88,7 +88,7 @@ final class GridControls extends ViewAndControls {
             new TitledPane(vocabulary.getString(Vocabulary.Keys.Display), 
displayPane)
             // TODO: more controls to be added in a future version.
         };
-        setView(view, view.statusBar);
+        setView(view);
     }
 
     /**
@@ -109,7 +109,6 @@ final class GridControls extends ViewAndControls {
      * @param  resource  the new source of coverage, or {@code null} if none.
      * @param  coverage  the new coverage, or {@code null} if none.
      */
-    @Override
     final void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
         final ObservableList<SampleDimension> items = 
sampleDimensions.getItems();
         if (coverage != null) {
@@ -118,7 +117,7 @@ final class GridControls extends ViewAndControls {
         } else {
             items.clear();
         }
-        super.notifyDataChanged(resource, coverage);
+        owner.notifyDataChanged(resource, coverage);
     }
 
     /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
index a215db2ddb..c30fd9d819 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
@@ -78,6 +78,8 @@ import org.apache.sis.util.resources.Vocabulary;
 public class GridSliceSelector extends Widget {
     /**
      * Constants used for identifying the code assuming a two-dimensional 
space.
+     *
+     * @see #getXYDimensions()
      */
     private static final int BIDIMENSIONAL = 2;
 
@@ -110,6 +112,14 @@ public class GridSliceSelector extends Widget {
      */
     private final ReadOnlyObjectWrapper<GridExtent> selectedExtent;
 
+    /**
+     * Indices of {@link GridExtent} dimensions which are assigned to 
<var>x</var> and <var>y</var> coordinates.
+     * They are usually 0 for <var>x</var> and 1 for <var>y</var>.
+     *
+     * @see #BIDIMENSIONAL
+     */
+    private int xDimension, yDimension;
+
     /**
      * The locale to use for axis labels, or {@code null} for a default locale.
      */
@@ -172,6 +182,7 @@ public class GridSliceSelector extends Widget {
         selectedExtent = new ReadOnlyObjectWrapper<>(this, "selectedExtent");
         gridGeometry = new SimpleObjectProperty<>(this, "gridGeometry");
         gridGeometry.addListener((p,o,n) -> setGridGeometry(n));
+        yDimension = 1;
     }
 
     /**
@@ -199,7 +210,11 @@ public class GridSliceSelector extends Widget {
         for (int dim=0; dim < dimension; dim++) {
             final long min = extent.getLow (dim);
             final long max = extent.getHigh(dim);
-            if (min < max && ++row >= 0) {
+            if (min < max) {
+                switch (++row) {
+                    case -2: xDimension = dim; continue;
+                    case -1: yDimension = dim; continue;
+                }
                 /*
                  * A new slider needs to be shown. Recycle existing slider and 
label if any,
                  * or create new controls if we already used all existing 
controls.
@@ -595,6 +610,17 @@ public class GridSliceSelector extends Widget {
         return selectedExtent.getReadOnlyProperty();
     }
 
+    /**
+     * Returns the grid dimensions of <var>x</var> and <var>y</var> axes 
rendered in a two-dimensional image or table.
+     * This value is inferred from the {@linkplain #gridGeometry grid 
geometry} property value.
+     * It is almost always {0,1}, i.e. the 2 first dimensions in a coordinate 
tuple.
+     *
+     * @return indices of <var>x</var> and <var>y</var> coordinate values in a 
grid coordinate tuple.
+     */
+    public final int[] getXYDimensions() {
+        return new int[] {xDimension, yDimension};
+    }
+
     /**
      * Returns the encapsulated JavaFX component to add in a scene graph for 
making the selectors visible.
      * The {@code Region} subclass is implementation dependent and may change 
in any future SIS version.
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 d0ddb608bd..00ee0eb64a 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
@@ -36,13 +36,16 @@ import javafx.scene.control.Skin;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.LogHandler;
 import org.apache.sis.internal.gui.Styles;
-import org.apache.sis.gui.map.StatusBar;
-import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.gui.ExceptionReporter;
 
 
 /**
@@ -56,7 +59,7 @@ 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.2
+ * @version 1.3
  *
  * @see CoverageExplorer
  *
@@ -201,11 +204,6 @@ public class GridView extends Control {
      */
     final CellFormat cellFormat;
 
-    /**
-     * The status bar where to show coordinates of selected cell.
-     */
-    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.
@@ -220,17 +218,16 @@ public class GridView extends Control {
      * construction by a call to {@link #setImage(RenderedImage)}.
      */
     public GridView() {
-        this(null, null);
+        this(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.
+     * @param  controls  the controls of this grid view, or {@code null} if 
none.
      */
-    GridView(final GridControls controls, final RecentReferenceSystems 
referenceSystems) {
+    GridView(final GridControls controls) {
         this.controls    = controls;
         bandProperty     = new BandProperty();
         imageProperty    = new SimpleObjectProperty<>(this, "image");
@@ -241,7 +238,6 @@ public class GridView extends Control {
         headerBackground = new SimpleObjectProperty<>(this, 
"headerBackground", Color.GAINSBORO);
         headerFormat     = NumberFormat.getIntegerInstance();
         cellFormat       = new CellFormat(this);
-        statusBar        = new StatusBar(referenceSystems);
         tiles            = new GridTileCache();
         tileWidth        = 1;
         tileHeight       = 1;       // For avoiding division by zero.
@@ -334,15 +330,15 @@ public class GridView extends Control {
     /**
      * 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.
+     * @param  resource  the new source of coverage, or {@code null} if none.
+     * @param  coverage  the new coverage, or {@code null} if none.
+     * @param  image     the loaded image, or {@code null} on failure.
      */
-    private void setLoadedImage(final ImageRequest request, final 
RenderedImage image) {
+    private void setLoadedImage(GridCoverageResource resource, GridCoverage 
coverage, RenderedImage image) {
         loader = null;          // Must be first for preventing cancellation.
         setImage(image);
-        request.configure(statusBar);
         if (controls != null) {
-            controls.notifyDataChanged(request.resource, 
request.getCoverage().orElse(null));
+            controls.notifyDataChanged(resource, coverage);
         }
     }
 
@@ -357,6 +353,12 @@ public class GridView extends Control {
          */
         private final ImageRequest request;
 
+        /**
+         * The coverage that has been read.
+         * It may either be specified explicitly in the {@link #request}, or 
read from the resource.
+         */
+        private GridCoverage coverage;
+
         /**
          * Creates a new task for loading an image from the specified coverage 
resource.
          *
@@ -367,15 +369,29 @@ public class GridView extends Control {
         }
 
         /**
-         * 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.
+         * Invoked in a background thread for loading the image.
          *
          * @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);
+        protected RenderedImage call() throws Exception {
+            final Long id = LogHandler.loadingStart(request.resource);
+            try {
+                coverage = request.load().forConvertedValues(true);
+                if (isCancelled()) {
+                    return null;
+                }
+                GridExtent slice = request.slice;
+                final GridControls c = controls;
+                if (c != null) {
+                    final GridGeometry gg = coverage.getGridGeometry();
+                    slice = BackgroundThreads.runAndWait(() -> 
c.setGeometry(gg));
+                }
+                return coverage.render(slice);
+            } finally {
+                LogHandler.loadingStop(id);
+            }
         }
 
         /**
@@ -384,7 +400,15 @@ public class GridView extends Control {
          */
         @Override
         protected void succeeded() {
-            setLoadedImage(request, getValue());
+            setLoadedImage(request.resource, coverage, getValue());
+        }
+
+        /**
+         * Invoked in JavaFX thread on cancellation. This method clears all 
controls.
+         */
+        @Override
+        protected void cancelled() {
+            setLoadedImage(null, null, null);
         }
 
         /**
@@ -393,8 +417,8 @@ public class GridView extends Control {
          */
         @Override
         protected void failed() {
-            setLoadedImage(request, null);
-            request.reportError(GridView.this, getException());
+            cancelled();
+            ExceptionReporter.canNotReadFile(GridView.this, request.resource, 
getException());
         }
     }
 
@@ -627,7 +651,18 @@ public class GridView extends Control {
      * Then the pixel coordinates are converted to "real world" coordinates 
and formatted.
      */
     final void formatCoordinates(final int x, final int y) {
-        statusBar.setLocalCoordinates(minX + x, minY + y);
+        if (controls != null) {
+            controls.status.setLocalCoordinates(minX + x, minY + y);
+        }
+    }
+
+    /**
+     * Hides coordinates in the status bar.
+     */
+    final void hideCoordinates() {
+        if (controls != null) {
+            controls.status.handle(null);
+        }
     }
 
     /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
index 00220a5de4..c33f959b1d 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
@@ -242,7 +242,7 @@ final class GridViewSkin extends 
VirtualContainerBase<GridView, GridRow> impleme
         selectedRow   .setVisible(visible);
         selectedColumn.setVisible(visible);
         if (!visible) {
-            getSkinnable().statusBar.handle(null);
+            getSkinnable().hideCoordinates();
         }
     }
 
@@ -315,7 +315,7 @@ final class GridViewSkin extends 
VirtualContainerBase<GridView, GridRow> impleme
         selection     .setVisible(false);
         selectedRow   .setVisible(false);
         selectedColumn.setVisible(false);
-        getSkinnable().statusBar.handle(null);      // Hide the coordinates.
+        getSkinnable().hideCoordinates();
     }
 
     /**
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 1f09e4c607..d9f1c58e09 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
@@ -17,20 +17,12 @@
 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.opengis.referencing.operation.TransformException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 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.storage.DataStoreException;
-import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -39,7 +31,7 @@ import org.apache.sis.storage.event.StoreListeners;
  * {@linkplain GridCoverage#render(GridExtent) rendering} and image in a 
background thread.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see GridView#setImage(ImageRequest)
  * @see CoverageExplorer#setCoverage(ImageRequest)
@@ -49,19 +41,18 @@ import org.apache.sis.storage.event.StoreListeners;
  */
 public class ImageRequest {
     /**
-     * The source from where to read the image, specified at construction time.
-     * May be {@code null} if {@link #coverage} instance was specified at 
construction time.
+     * The source from where to read the image.
+     * One of {@code resource} and {@link #coverage} fields is non-null.
      */
     final GridCoverageResource resource;
 
     /**
-     * The source for rendering the image, specified at construction time.
-     * 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.
+     * The source for rendering the image.
+     * One of {@link #resource} and {@code coverage} fields is non-null.
      *
      * @see #getCoverage()
      */
-    private volatile GridCoverage coverage;
+    final GridCoverage coverage;
 
     /**
      * Desired grid extent and resolution, or {@code null} for reading the 
whole domain.
@@ -82,12 +73,10 @@ public class ImageRequest {
     /**
      * A subspace of the grid coverage extent to render, or {@code null} for 
the whole extent.
      * It can be used for specifying a slice in a <var>n</var>-dimensional 
data cube.
-     * If not specified by the user, will be updated to the extent actually 
rendered.
      *
      * @see #getSliceExtent()
-     * @see #setSliceExtent(GridExtent)
      */
-    private volatile GridExtent sliceExtent;
+    final GridExtent slice;
 
     /**
      * Creates a new request with both a resource and a coverage. At least one 
argument shall be non-null.
@@ -103,6 +92,7 @@ public class ImageRequest {
         coverage = data;
         domain   = null;
         range    = null;
+        slice    = null;
     }
 
     /**
@@ -127,6 +117,8 @@ public class ImageRequest {
          * GridCoverageResource, which is the class making real use of it. 
This is not sensitive
          * object state here.
          */
+        this.coverage = null;
+        this.slice    = null;
     }
 
     /**
@@ -136,25 +128,33 @@ public class ImageRequest {
      * or for rendering data along other dimensions, a slice extent can be 
specified as documented in the
      * {@linkplain GridCoverage#render(GridExtent) render method javadoc}.
      *
-     * @param  source       source of the image to load.
-     * @param  sliceExtent  a subspace of the grid coverage extent to render, 
or {@code null} for the whole extent.
+     * @param  source  source of the image to load.
+     * @param  slice   a subspace of the grid coverage extent to render, or 
{@code null} for the whole extent.
      *
      * @see GridCoverage#render(GridExtent)
      */
-    public ImageRequest(final GridCoverage source, final GridExtent 
sliceExtent) {
+    public ImageRequest(final GridCoverage source, final GridExtent slice) {
         ArgumentChecks.ensureNonNull("source", source);
-        this.resource    = null;
-        this.domain      = null;
-        this.range       = null;
-        this.coverage    = source;
-        this.sliceExtent = sliceExtent;
+        this.resource = null;
+        this.domain   = null;
+        this.range    = null;
+        this.coverage = source;
+        this.slice    = slice;
+    }
+
+    /**
+     * Returns the resource specified at construction time, or an empty value 
if none.
+     *
+     * @return the resource to read.
+     */
+    public final Optional<GridCoverageResource> getResource() {
+        return Optional.ofNullable(resource);
     }
 
     /**
-     * Returns the coverage, or an empty value if not yet known. This is 
either the value specified explicitly
-     * to the constructor, or otherwise the coverage obtained after a read 
operation.
+     * Returns the coverage specified at construction time, or an empty value 
if none.
      *
-     * @return the coverage.
+     * @return the coverage to render.
      */
     public final Optional<GridCoverage> getCoverage() {
         return Optional.ofNullable(coverage);
@@ -209,141 +209,35 @@ public class ImageRequest {
 
     /**
      * Returns the subspace of the grid coverage extent to render.
-     * This method returns the first non-empty value in the following choices:
+     * This is the {@code sliceExtent} argument specified to the following 
constructor:
      *
-     * <ol>
-     *   <li>The last value specified to {@link 
#setSliceExtent(GridExtent)}.</li>
-     *   <li>The value specified to the {@link #ImageRequest(GridCoverage, 
GridExtent)} constructor.</li>
-     *   <li>The extent of the default slice selected by this {@code 
ImageRequest}
-     *       after completion of the reading task.</li>
-     * </ol>
+     * <blockquote>{@link #ImageRequest(GridCoverage, GridExtent)}</blockquote>
      *
-     * @return subspace of the grid coverage extent to render.
+     * This argument will be forwarded verbatim to the following method
+     * (see its javadoc for more explanation):
      *
-     * @see GridCoverage#render(GridExtent)
-     */
-    public final Optional<GridExtent> getSliceExtent() {
-        return Optional.ofNullable(sliceExtent);
-    }
-
-    /**
-     * Sets a new subspace of the grid coverage extent to render.
-     * This method can be used for specifying a two-dimensional slice in a 
<var>n</var>-dimensional data cube,
-     * as specified in {@link GridCoverage#render(GridExtent)} documentation.
+     * <blockquote>{@link GridCoverage#render(GridExtent)}</blockquote>
      *
-     * <div class="note"><b>API design note:</b>
-     * this {@code sliceExtent} argument is not specified
-     * to the {@link #ImageRequest(GridCoverageResource, GridGeometry, int[])} 
constructor because when reading data
-     * from a {@link GridCoverageResource}, a slicing can already be done by 
the {@link GridGeometry} {@code domain}
-     * argument. This method is provided for the rare cases where it may be 
useful to specify both the {@code domain}
-     * and the {@code sliceExtent}. The difference between the two ways to 
specify a slice is that the {@code domain}
-     * argument is used at reading time for reducing the amount of data to 
load, while this {@link sliceExtent}
-     * property is typically used after data has been read.</div>
+     * If non-empty, then all dimensions except two should have a size of 1 
cell.
      *
-     * @param  sliceExtent  subspace of the grid coverage extent to render, or 
{@code null} for the whole extent.
-     *         All dimensions except two shall have a size of 1 cell.
+     * @return subspace of the grid coverage extent to render.
      *
      * @see GridCoverage#render(GridExtent)
      */
-    public final void setSliceExtent(final GridExtent sliceExtent) {
-        this.sliceExtent = sliceExtent;
-    }
-
-    /**
-     * 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>
-     *
-     * <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} (because usually 
not needed).
-     *
-     * @param  task  the task invoking this method (for checking for 
cancellation).
-     * @return the image loaded from the source given at construction time, or 
{@code null}
-     *         if the task has been cancelled.
-     * @throws DataStoreException if an error occurred while loading the grid 
coverage.
-     */
-    final synchronized RenderedImage load(final FutureTask<?> task) throws 
DataStoreException {
-        GridCoverage cv = coverage;
-        final Long id = LogHandler.loadingStart(resource);
-        try {
-            if (cv == null) {
-                cv = MultiResolutionImageLoader.getInstance(resource, 
null).getOrLoad(domain, range);
-            }
-            coverage = cv = cv.forConvertedValues(true);
-            GridExtent ex = sliceExtent;
-            if (ex == null) {
-                final GridGeometry gg = cv.getGridGeometry();
-                if (gg.isDefined(GridGeometry.EXTENT)) {
-                    ex = gg.getExtent();
-                    if (gg.getDimension() > 
MultiResolutionImageLoader.BIDIMENSIONAL) {
-                        ex = MultiResolutionImageLoader.slice(gg.derive(), 
ex).getIntersection();
-                    }
-                    sliceExtent = ex;
-                }
-            }
-            if (task.isCancelled()) {
-                return null;
-            }
-            return cv.render(ex);
-        } finally {
-            LogHandler.loadingStop(id);
-        }
-    }
-
-    /**
-     * Configures the given status bar with the geometry of the grid coverage 
we have just read.
-     * This method is invoked in JavaFX thread after above {@link 
#load(FutureTask)} background
-     * task completed, regardless if successful or not.
-     * The two method calls are done (indirectly) by {@link 
GridView#setImage(ImageRequest)}.
-     */
-    final void configure(final StatusBar bar) {
-        final Long id = LogHandler.loadingStart(resource);
-        try {
-            GridExtent ex = sliceExtent;
-            GridCoverage cv = coverage;
-            GridGeometry gg = (cv != null) ? cv.getGridGeometry() : null;
-            if (gg != null && ex != null) {
-                /*
-                 * By `GridCoverage.render(GridExtent)` contract, the 
`RenderedImage` pixel coordinates are relative
-                 * to the requested `GridExtent`. Consequently we need to 
translate the grid coordinates so that the
-                 * request coordinates start at zero.
-                 */
-                final long[] offset = new long[ex.getDimension()];
-                for (final int i : bar.getXYDimensions()) {
-                    offset[i] = Math.negateExact(ex.getLow(i));
-                }
-                ex = ex.translate(offset);
-                gg = gg.translate(offset);          // Does not change the 
"real world" envelope.
-                try {
-                    gg = gg.relocate(ex);           // Changes the "real 
world" envelope.
-                } catch (TransformException e) {
-                    bar.setErrorMessage(null, e);
-                }
-            }
-            bar.applyCanvasGeometry(gg);
-        } finally {
-            LogHandler.loadingStop(id);
-        }
+    public final Optional<GridExtent> getSliceExtent() {
+        return Optional.ofNullable(slice);
     }
 
     /**
-     * Reports an exception in a dialog box. This is a convenience method for
-     * {@link javafx.concurrent.Task#succeeded()} implementations.
+     * Returns or loads the coverage. This method should be invoked in a 
background thread.
      *
-     * @param  owner      control in the window which will own the dialog, or 
{@code null} if unknown.
-     * @param  exception  the error that occurred.
+     * @return the coverage. May be a cached instance.
+     * @throws DataStoreException if an error occurred during the loading 
process.
      */
-    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);
+    final GridCoverage load() throws DataStoreException {
+        if (coverage != null) {
+            return coverage;
         }
+        return MultiResolutionImageLoader.getInstance(resource, 
null).getOrLoad(domain, range);
     }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
index 10283fdd33..e42e6da83e 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java
@@ -20,14 +20,11 @@ import java.util.WeakHashMap;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.coverage.grid.GridDerivation;
-import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.map.coverage.MultiResolutionCoverageLoader;
 
 
 /**
- * A helper class for reading two-dimensional slices of {@link GridCoverage}.
+ * A helper class for reading {@link GridCoverage} for rendering purposes.
  * The same instance may be shared by {@link GridView} and {@link 
CoverageCanvas}.
  * {@code GridView} uses only level 0, while {@code CoverageCanvas} use any 
level.
  *
@@ -35,29 +32,12 @@ import 
org.apache.sis.internal.map.coverage.MultiResolutionCoverageLoader;
  * Instances of this class are immutable (except for the cache) and safe for 
use by multiple threads.
  * The same instance may be shared by many {@link CoverageCanvas} or {@link 
GridView} objects.
  *
- * <h2>Limitations (TODO)</h2>
- * Current implementation reads only the two first dimensions.
- * We will need to define an API for specifying which dimensions to use for 
the slices.
- *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
 final class MultiResolutionImageLoader extends MultiResolutionCoverageLoader {
-    /**
-     * The {@value} value, for identifying code that assume two-dimensional 
objects.
-     */
-    static final int BIDIMENSIONAL = 2;
-
-    /**
-     * The relative position of slice in dimensions other than the 2 visible 
dimensions,
-     * as a ratio between 0 and 1. This may become configurable in a future 
version.
-     *
-     * @see GridDerivation#sliceByRatio(double, int[])
-     */
-    private static final double SLICE_RATIO = 0;
-
     /**
      * The loaders created for grid coverage resources.
      */
@@ -103,54 +83,4 @@ final class MultiResolutionImageLoader extends 
MultiResolutionCoverageLoader {
         }
         return cached;
     }
-
-    /**
-     * Given a {@code GridGeometry} configured with the resolution to read, 
returns an amended domain
-     * for a two-dimensional slice.
-     *
-     * @param  subgrid  a grid geometry with the desired resolution.
-     * @return the domain to read from the {@linkplain #resource resource}.
-     */
-    @Override
-    protected GridGeometry getReadDomain(final GridGeometry subgrid) {
-        return slice(subgrid);
-    }
-
-    /**
-     * Returns the given grid geometry with grid indices narrowed to a two 
dimensional slice.
-     * If more than two dimensions are eligible, this method selects the 2 
first ones.
-     *
-     * @param  gg  the grid geometry to reduce to two dimensions, or {@code 
null}.
-     * @return the given grid geometry reduced to 2 dimensions, or {@code 
null} if the geometry was null.
-     */
-    static GridGeometry slice(GridGeometry gg) {
-        if (gg != null && gg.getDimension() > BIDIMENSIONAL && 
gg.isDefined(GridGeometry.EXTENT)) {
-            gg = slice(gg.derive(), gg.getExtent()).build();
-        }
-        return gg;
-    }
-
-    /**
-     * Configures the given {@link GridDerivation} for applying a 
two-dimensional slice.
-     * This method selects the two first dimensions having a size greater than 
1 cell.
-     *
-     * @param  subgrid  a grid geometry builder pre-configured with the 
desired resolution.
-     * @param  extent   extent of the coverage to read, in units of the finest 
level.
-     * @return the builder configured for returning the desired 
two-dimensional slice.
-     */
-    static GridDerivation slice(final GridDerivation subgrid, final GridExtent 
extent) {
-        final int dimension = extent.getDimension();
-        if (dimension <= BIDIMENSIONAL) {
-            return subgrid;
-        }
-        final int[] sliceDimensions = new int[BIDIMENSIONAL];
-        int k = 0;
-        for (int i=0; i<dimension; i++) {
-            if (extent.getLow(i) != extent.getHigh(i)) {
-                sliceDimensions[k] = i;
-                if (++k >= BIDIMENSIONAL) break;
-            }
-        }
-        return subgrid.sliceByRatio(SLICE_RATIO, sliceDimensions);
-    }
 }
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 ff6e9bd8a5..89a993c829 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
@@ -35,8 +35,10 @@ import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.gui.map.StatusBar;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.util.resources.IndexedResourceBundle;
+import org.apache.sis.gui.map.StatusBar;
 
 
 /**
@@ -106,12 +108,17 @@ abstract class ViewAndControls {
      */
     protected final GridSliceSelector sliceSelector;
 
+    /**
+     * The status bar where to show cursor coordinates.
+     */
+    protected final StatusBar status;
+
     /**
      * The widget which contain this view. This is the widget to inform when 
the coverage changed.
      *
-     * @see #notifyDataChanged(GridCoverageResource, GridCoverage)
+     * @see CoverageExplorer#notifyDataChanged(GridCoverageResource, 
GridCoverage)
      */
-    private final CoverageExplorer owner;
+    protected final CoverageExplorer owner;
 
     /**
      * Creates a new view-control pair.
@@ -120,10 +127,11 @@ abstract class ViewAndControls {
      */
     protected ViewAndControls(final CoverageExplorer owner) {
         this.owner = owner;
+        status = new StatusBar(owner.referenceSystems);
         sliceSelector = new GridSliceSelector(owner.getLocale());
         viewAndNavigation = new VBox();
         sliceSelector.selectedExtentProperty().addListener((p,o,n) -> {
-            final GridCoverage coverage = 
ViewAndControls.this.owner.getCoverage();
+            final GridCoverage coverage = getCoverage();
             if (coverage != null) {
                 load(new ImageRequest(coverage, n));    // Show a new slice of 
data.
             }
@@ -134,7 +142,7 @@ abstract class ViewAndControls {
      * Invoked by subclass constructors for declaring the main visual 
component.
      * The given component will be added to the {@link #viewAndNavigation} 
node.
      */
-    final void setView(final Region view, final StatusBar status) {
+    final void setView(final Region view) {
         final Region bar = status.getView();
         final Region nav = sliceSelector.getView();
         VBox.setVgrow(view, Priority.ALWAYS);
@@ -160,6 +168,13 @@ abstract class ViewAndControls {
         return controls;
     }
 
+    /**
+     * Returns the grid coverage shown in the view, or {@code null} if none.
+     */
+    GridCoverage getCoverage() {
+        return owner.getCoverage();
+    }
+
     /**
      * Sets the view content to the given resource, coverage or image.
      * This method is invoked when a new source of data (either a resource or 
a coverage) is specified,
@@ -170,14 +185,15 @@ abstract class ViewAndControls {
     abstract void load(ImageRequest request);
 
     /**
-     * Notifies all controls that a new coverage has been loaded.
-     * Subclasses shall invoke this method in the JavaFX thread after loading 
completed.
+     * Invoked when a new coverage or coverage resource has been specified.
+     * This method configures the status bar, adjusts the sliders and returns
+     * the new selected slice. This method shall be invoked in JavaFX thread.
      *
-     * @param  resource  the new source of coverage, or {@code null} if none.
-     * @param  coverage  the new coverage, or {@code null} if none.
+     * @param  geometry  grid geometry of the coverage or resource, or {@code 
null} if none.
+     * @return new slice to take as the currently selected slice.
      */
-    void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
-        sliceSelector.gridGeometry.set(coverage != null ? 
coverage.getGridGeometry() : null);
+    final GridExtent setGeometry(final GridGeometry geometry) {
+        sliceSelector.gridGeometry.set(geometry);
         final ObservableList<Node> components = 
viewAndNavigation.getChildren();
         final int count = components.size();
         if (sliceSelector.isEmpty()) {
@@ -189,7 +205,11 @@ abstract class ViewAndControls {
                 components.add(sliceSelector.getView());
             }
         }
-        owner.notifyDataChanged(resource, coverage);
+        // The selected slice changed as a result of new grid geometry.
+        final GridExtent slice = 
sliceSelector.selectedExtentProperty().getValue();
+        final int[] xyDimensions = sliceSelector.getXYDimensions();
+        status.applyCanvasGeometry(geometry, slice, xyDimensions[0], 
xyDimensions[1]);
+        return slice;
     }
 
 
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
index cce34c15a0..f8595e285b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.function.Predicate;
+import java.awt.image.RenderedImage;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 import javax.measure.Unit;
@@ -65,6 +66,7 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.geometry.CoordinateFormat;
+import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.portrayal.RenderException;
@@ -162,7 +164,7 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      * <p>Note that if this field is non-null, then the {@link 
#localToObjectiveCRS} property value may be overwritten
      * at any time, for example every time that a gesture event such as pan, 
zoom or rotation happens.</p>
      */
-    private final MapCanvas canvas;
+    private MapCanvas canvas;
 
     /**
      * The manager of reference systems chosen by user, or {@code null} if 
none.
@@ -212,7 +214,7 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      * <p>This transform shall never be null. It is initially an identity 
transform and is modified by
      * {@link #applyCanvasGeometry(GridGeometry)}. The transform is usually 
(but not necessarily) affine
      * and should have no {@linkplain 
CoordinateOperation#getCoordinateOperationAccuracy() inaccuracy}
-     * (ignoring rounding error). This is normally the inverse of {@linkplain 
#canvas}
+     * (ignoring rounding error). This transform is normally the inverse of 
{@linkplain #canvas}
      * {@linkplain MapCanvas#getObjectiveToDisplay() objective to display} 
transform,
      * but temporary mismatches may exist during gesture events such as pans, 
zooms and rotations.</p>
      *
@@ -272,6 +274,14 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      */
     private Predicate<MapCanvas> fullOperationSearchRequired;
 
+    /**
+     * Indices where to assign the values of the <var>x</var> and <var>y</var> 
arguments in {@link #sourceCoordinates}.
+     * They are usually 0 for <var>x</var> and 1 for <var>y</var>.
+     *
+     * @see #BIDIMENSIONAL
+     */
+    private int xDimension, yDimension;
+
     /**
      * The source local indices before conversion to geospatial coordinates 
(never {@code null}).
      * The number of dimensions is often {@value #BIDIMENSIONAL}. May be the 
same array than
@@ -384,12 +394,12 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
 
     /**
      * Creates a new status bar for showing coordinates of mouse cursor 
position in a canvas.
-     * If the {@code canvas} argument is non-empty, this {@code StatusBar} 
will show coordinates
+     * If {@link #track(Canvas)} is invoked, then this {@code StatusBar} will 
show coordinates
      * (usually geographic or projected) of mouse cursor position when the 
mouse is over that canvas.
      * Note that in such case, the {@link #localToObjectiveCRS} property value 
will be overwritten
      * at any time (for example every time that a gesture event such as pan, 
zoom or rotation happens).
      *
-     * <p>If the {@code choices} argument is non-null, user will be able to 
select different CRS
+     * <p>If the {@code systemChooser} argument is non-null, user will be able 
to select different CRS
      * using the contextual menu on the status bar.</p>
      *
      * <h4>Limitations</h4>
@@ -398,21 +408,16 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      * to exist as long as the {@code MapCanvas} and {@code 
RecentReferenceSystems} instances
      * given to this constructor.
      *
-     * <p>Current implementation accepts only zero or one {@code MapCanvas}. A 
future implementation
-     * may accept a larger amount of canvas for tracking many views with a 
single status bar
-     * (for example images over the same area but at different times).</p>
-     *
      * @param  systemChooser  the manager of reference systems chosen by user, 
or {@code null} if none.
-     * @param  toTrack        the canvas that this status bar is tracking.
-     *                        Currently restricted to an array of length 0 or 
1.
      */
-    public StatusBar(final RecentReferenceSystems systemChooser, final 
MapCanvas... toTrack) {
+    public StatusBar(final RecentReferenceSystems systemChooser) {
         positionReferenceSystem = new PositionSystem();
         localToObjectiveCRS     = new LocalToObjective();
         localToPositionCRS      = localToObjectiveCRS.get();
         targetCoordinates       = new GeneralDirectPosition(BIDIMENSIONAL);
         sourceCoordinates       = targetCoordinates.coordinates;
         lastX = lastY           = Double.NaN;
+        yDimension              = 1;
         format                  = new CoordinateFormat();
 
         message = new Label();
@@ -485,42 +490,69 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
             if (n != null) items.add(0, n.valueChoices);
             setSampleValuesVisible(n != null && !n.isEmpty());
         });
+    }
+
+    /**
+     * @deprecated Replaced by {@link #StatusBar(RecentReferenceSystems)} 
followed by {@link #track(MapCanvas)}.
+     *
+     * @param  systemChooser  the manager of reference systems chosen by user, 
or {@code null} if none.
+     * @param  toTrack        the canvas that this status bar is tracking.
+     */
+    @Deprecated
+    public StatusBar(final RecentReferenceSystems systemChooser, final 
MapCanvas... toTrack) {
+        this(systemChooser);
+        for (final MapCanvas canvas : toTrack) {
+            track(canvas);
+        }
+    }
+
+    /**
+     * Registers listeners on the following canvas for track mouse movements.
+     * After this method call, this {@code StatusBar} will show coordinates 
(usually geographic or projected)
+     * of mouse cursor position when the mouse is over that canvas. The {@link 
#localToObjectiveCRS} property
+     * value may be overwritten at any time, for example after each gesture 
event such as pan, zoom or rotation.
+     *
+     * <h4>Limitations</h4>
+     * Current implementation accepts only zero or one {@code MapCanvas}. A 
future implementation
+     * may accept a larger amount of canvas for tracking many views with a 
single status bar
+     * (for example images over the same area but at different times).
+     *
+     * @param  canvas  the canvas that this status bar is tracking.
+     *
+     * @since 1.3
+     */
+    public void track(final MapCanvas canvas) {
+        ArgumentChecks.ensureNonNull("canvas", canvas);
+        if (this.canvas != null) {
+            throw new IllegalArgumentException(Errors.format(
+                        Errors.Keys.TooManyCollectionElements_3, "canvas", 1, 
2));
+        }
         /*
          * If a canvas is specified, register listeners for mouse position, 
rendering events, errors, etc.
          * We do not allow the canvas to be changed after construction because 
of the added complexity
          * (e.g. we would have to remember all registered listeners so we can 
unregister them).
          */
-        if (toTrack != null && toTrack.length != 0) {
-            if (toTrack.length != 1) {
-                throw new IllegalArgumentException(Errors.format(
-                        Errors.Keys.TooManyCollectionElements_3, "toTrack", 1, 
toTrack.length));
-            }
-            canvas = toTrack[0];
+        this.canvas = canvas;
+        sampleValuesProvider.set(ValuesUnderCursor.create(canvas));
+        canvas.errorProperty().addListener((p,o,n) -> setRenderingError(n));
+        canvas.renderingProperty().addListener((p,o,n) -> {if (!n) 
applyCanvasGeometry();});
+        applyCanvasGeometry();
+        if (canvas.getObjectiveCRS() != null) {
+            registerMouseListeners();
         } else {
-            canvas = null;
-        }
-        if (canvas != null) {
-            sampleValuesProvider.set(ValuesUnderCursor.create(canvas));
-            canvas.errorProperty().addListener((p,o,n) -> 
setRenderingError(n));
-            canvas.renderingProperty().addListener((p,o,n) -> {if (!n) 
applyCanvasGeometry();});
-            applyCanvasGeometry();
-            if (canvas.getObjectiveCRS() != null) {
-                registerMouseListeners();
-            } else {
-                /*
-                 * Wait for objective CRS to be known before to register 
listeners.
-                 * The canvas "objective CRS" is null only for unitialized 
canvas.
-                 * After the canvas has been initialized, it can not be null 
anymore.
-                 * We delay listeners registration because if listeners were 
enabled
-                 * on uninitialized canvas, the status bar would show 
irrelevant coordinates.
-                 */
-                
canvas.addPropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, new 
PropertyChangeListener() {
-                    @Override public void propertyChange(final 
PropertyChangeEvent event) {
-                        
canvas.removePropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, this);
-                        registerMouseListeners();
-                    }
-                });
-            }
+            /*
+             * Wait for objective CRS to be known before to register listeners.
+             * The canvas "objective CRS" is null only for unitialized canvas.
+             * After the canvas has been initialized, it can not be null 
anymore.
+             * We delay listeners registration because if listeners were 
enabled
+             * on uninitialized canvas, the status bar would show irrelevant 
coordinates.
+             */
+            canvas.addPropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, 
new PropertyChangeListener() {
+                @Override public void propertyChange(final PropertyChangeEvent 
event) {
+                    
canvas.removePropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, this);
+                    registerMouseListeners();
+                }
+            });
         }
     }
 
@@ -584,6 +616,50 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      */
     public void applyCanvasGeometry(final GridGeometry geometry) {
         position.setText(null);
+        xDimension = 0;
+        yDimension = 1;
+        apply(geometry);
+    }
+
+    /**
+     * Configures this status bar for showing coordinates of a slice of a grid 
coverage.
+     * This method is useful for tracking the pixel coordinates of an image 
obtained by
+     * a call to {@link GridCoverage#render(GridExtent)}.
+     * By {@code render(GridExtent)} contract, the {@link RenderedImage} pixel 
coordinates
+     * are relative to the requested {@link GridExtent}. Consequently we need 
to translate
+     * the grid coordinates so that the request coordinates start at zero.
+     * This method handles that translation.
+     *
+     * @param  geometry     geometry of the coverage which produced the {@link 
RenderedImage} to track, or {@code null}.
+     * @param  sliceExtent  the extent specified in call to {@link 
GridCoverage#render(GridExtent)} (can be {@code null}).
+     * @param  xdim         the grid dimension where to assign the values of 
<var>x</var> pixel coordinates.
+     * @param  ydim         the grid dimension where to assign the values of 
<var>y</var> pixel coordinates.
+     *
+     * @since 1.3
+     */
+    public void applyCanvasGeometry(GridGeometry geometry, GridExtent 
sliceExtent, final int xdim, final int ydim) {
+        position.setText(null);
+        if (geometry != null) {
+            final int dimension = geometry.getDimension();
+            ArgumentChecks.ensureDimensionMatches("sliceExtent", dimension, 
sliceExtent);
+            ArgumentChecks.ensureBetween("xdim", 0,      dimension-1, xdim);
+            ArgumentChecks.ensureBetween("ydim", xdim+1, dimension-1, ydim);
+            xDimension = xdim;
+            yDimension = ydim;                      // Shall be assigned 
before call to `getXYDimensions()` below.
+            if (sliceExtent != null) {
+                final long[] offset = new long[dimension];
+                for (final int i : getXYDimensions()) {
+                    offset[i] = Math.negateExact(sliceExtent.getLow(i));
+                }
+                sliceExtent = sliceExtent.translate(offset);
+                geometry = geometry.translate(offset);              // Does 
not change the "real world" envelope.
+                try {
+                    geometry = geometry.relocate(sliceExtent);      // Changes 
the "real world" envelope.
+                } catch (TransformException e) {
+                    setErrorMessage(null, e);
+                }
+            }
+        }
         apply(geometry);
     }
 
@@ -989,15 +1065,16 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      * Returns the indices of <var>x</var> and <var>y</var> coordinate values 
in a grid coordinate tuple.
      * They are the indices where to assign the values of the <var>x</var> and 
<var>y</var> arguments in
      * calls to <code>{@linkplain #setLocalCoordinates(double, double) 
setLocalCoordinates}(x,y)</code>.
-     * The default value is {0,1}, i.e. the 2 first dimensions in a coordinate 
tuple.
+     *
+     * <p>The default value is {0,1}, i.e. the 2 first dimensions in a 
coordinate tuple.
+     * The value can be changed by call to {@link 
#applyCanvasGeometry(GridGeometry, GridExtent, int, int)}.</p>
      *
      * @return indices of <var>x</var> and <var>y</var> coordinate values in a 
grid coordinate tuple.
      *
      * @since 1.3
      */
     public final int[] getXYDimensions() {
-        // Fixed for now, future version may allow configuration.
-        return ArraysExt.range(0, BIDIMENSIONAL);
+        return new int[] {xDimension, yDimension};
     }
 
     /**
@@ -1059,8 +1136,8 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      */
     public void setLocalCoordinates(final double x, final double y) {
         if (x != lastX || y != lastY) {
-            sourceCoordinates[0] = lastX = x;
-            sourceCoordinates[1] = lastY = y;
+            sourceCoordinates[xDimension] = lastX = x;
+            sourceCoordinates[yDimension] = lastY = y;
             String text, values = null;
             try {
                 convertCoordinates();
@@ -1151,8 +1228,8 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
      * @param  y  the <var>y</var> coordinate local to the view.
      */
     final String formatCoordinates(final double x, final double y) throws 
TransformException {
-        sourceCoordinates[0] = x;
-        sourceCoordinates[1] = y;
+        sourceCoordinates[xDimension] = x;
+        sourceCoordinates[yDimension] = y;
         final String separator = format.getSeparator();
         try {
             format.setSeparator("\t");
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
index c5351dbb46..a685e33ac9 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
@@ -31,6 +31,7 @@ import org.apache.sis.gui.DataViewer;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.system.Threads;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.Exceptions;
 
 import static java.util.logging.Logger.getLogger;
 
@@ -48,7 +49,7 @@ import static java.util.logging.Logger.getLogger;
  * Users should not rely on this implementation details.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -135,7 +136,7 @@ public final class BackgroundThreads extends AtomicInteger 
implements ThreadFact
      * @param  task  the task to execute in JavaFX thread.
      * @return the task result, or {@code null} if an error occurred.
      */
-    public static <V> V runAndWait(final Callable<V> task) {
+    public static <V> V runAndWaitDialog(final Callable<V> task) {
         final FutureTask<V> f = new FutureTask<>(task);
         Platform.runLater(f);
         try {
@@ -148,6 +149,25 @@ public final class BackgroundThreads extends AtomicInteger 
implements ThreadFact
         return null;
     }
 
+    /**
+     * Runs the given task in JavaFX thread and wait for completion before to 
return.
+     * This method should <em>not</em> be invoked from JavaFX application 
thread.
+     *
+     * @param  <V>   type of result that will be returned.
+     * @param  task  the task to execute in JavaFX thread.
+     * @return the task result.
+     * @throws Exception if the task threw an exception.
+     */
+    public static <V> V runAndWait(final Callable<V> task) throws Exception {
+        final FutureTask<V> f = new FutureTask<>(task);
+        Platform.runLater(f);
+        try {
+            return f.get();
+        } catch (ExecutionException e) {
+            throw Exceptions.unwrap(e);
+        }
+    }
+
     /**
      * Invoked at application shutdown time for stopping the executor threads 
after they completed their tasks.
      * This method returns soon but the background threads may continue for 
some time if they did not finished
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
index 20bb02e8ae..20a6424bf3 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
@@ -41,6 +41,9 @@ import javafx.scene.layout.Region;
 import javafx.scene.layout.VBox;
 import org.apache.sis.gui.Widget;
 import org.apache.sis.util.Classes;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -49,7 +52,7 @@ import org.apache.sis.util.Classes;
  *
  * @author  Smaniotto Enzo (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -180,6 +183,27 @@ public final class ExceptionReporter extends Widget {
         }
     }
 
+    /**
+     * Shows the reporter for a failure to read a file.
+     * This method does nothing if the exception is null.
+     *
+     * @param  owner      control in the window which will own the dialog, or 
{@code null} if unknown.
+     * @param  resource   the resource that can not be read.
+     * @param  exception  the error that occurred.
+     */
+    public static void canNotReadFile(final Node owner, final Resource 
resource, final Throwable exception) {
+        final String name;
+        if (resource instanceof DataStore) {
+            name = ((DataStore) resource).getDisplayName();
+        } else if (resource instanceof StoreListeners) {
+            name = ((StoreListeners) resource).getSourceName();
+        } else {
+            canNotUseResource(owner, exception);
+            return;
+        }
+        canNotReadFile(owner, name, exception);
+    }
+
     /**
      * Shows the reporter for a failure to read a file.
      * This method does nothing if the exception is null.
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java
index d1b4f2c1b0..99195370a7 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java
@@ -64,7 +64,7 @@ public final class OptionalDataDownloader extends 
OptionalInstallations {
     @Override
     protected boolean askUserAgreement(final String authority, final String 
license) {
         if (!Platform.isFxApplicationThread()) {
-            return BackgroundThreads.runAndWait(() -> {
+            return BackgroundThreads.runAndWaitDialog(() -> {
                 return askUserAgreement(authority, license);
             });
         }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java
index 3534a98333..4434c2ca57 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java
@@ -25,7 +25,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
diff --git 
a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java
 
b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java
index ce23aaa092..ecf9709dd5 100644
--- 
a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java
+++ 
b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java
@@ -70,8 +70,8 @@ public class CoverageCanvasApp extends Application {
     @Override
     public void start(final Stage window) {
         final CoverageCanvas canvas = new CoverageCanvas();
-        final StatusBar statusBar = new StatusBar(null, canvas);
-        canvas.statusBar = statusBar;
+        final StatusBar statusBar = new StatusBar(null);
+        statusBar.track(canvas);
         canvas.setCoverage(createImage());
         final BorderPane pane = new BorderPane(canvas.getView());
         pane.setBottom(statusBar.getView());
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index 9bb02bc94a..c0ab6e9148 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -100,7 +100,7 @@ import static 
org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY;
  * Support for tiled images will be added in a future version.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see GridCoverage#render(GridExtent)
  *
@@ -123,6 +123,9 @@ public class ImageRenderer {
     /**
      * The dimensions to select in the grid coverage for producing an image. 
This is an array of length
      * {@value GridCoverage2D#BIDIMENSIONAL} obtained by {@link 
GridExtent#getSubspaceDimensions(int)}.
+     * The array content is almost always {0,1}, but this class should work 
with other dimensions too.
+     *
+     * @see #getXYDimensions()
      */
     private final int[] gridDimensions;
 
@@ -405,6 +408,19 @@ public class ImageRenderer {
         return new Rectangle(imageX, imageY, width, height);
     }
 
+    /**
+     * The dimensions to select in the grid coverage for producing an image. 
This is the array obtained
+     * by <code>{@link GridExtent#getSubspaceDimensions(int) 
GridExtent.getSubspaceDimensions(2)}</code>.
+     * The array content is almost always {0,1}, i.e. the 2 first dimensions 
in a coordinate tuple.
+     *
+     * @return indices of <var>x</var> and <var>y</var> coordinate values in a 
grid coordinate tuple.
+     *
+     * @since 1.3
+     */
+    public final int[] getXYDimensions() {
+        return gridDimensions.clone();
+    }
+
     /**
      * Computes the conversion from pixel coordinates to CRS, together with 
the geospatial envelope of the image.
      * The {@link GridGeometry} returned by this method is derived from the 
{@linkplain GridCoverage#getGridGeometry()
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
index 51ee06b053..d7dfa3d54b 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
@@ -33,7 +33,6 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.coverage.grid.GridDerivation;
 import org.apache.sis.coverage.grid.GridRoundingMode;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.math.DecimalFunctions;
@@ -294,7 +293,7 @@ dimensions: for (int j=0; j<tgtDim; j++) {
             final MathTransform gridToCRS = MathTransforms.scale(resolutions);
             domain = new GridGeometry(PixelInCell.CELL_CORNER, gridToCRS, 
areaOfInterest, GridRoundingMode.ENCLOSING);
         }
-        final GridCoverage coverage = resource.read(getReadDomain(domain), 
readRanges);
+        final GridCoverage coverage = resource.read(domain, readRanges);
         /*
          * Cache and return the coverage. The returned coverage may be a 
different instance
          * if another coverage has been cached concurrently for the same level.
@@ -313,7 +312,7 @@ dimensions: for (int j=0; j<tgtDim; j++) {
     /**
      * If the a grid coverage for the given domain and range is in the cache, 
returns that coverage.
      * Otherwise loads the coverage and eventually caches it. The caching 
happens only if the given
-     * domain and range and managed by this loader.
+     * domain and range are managed by this loader.
      *
      * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
      * @param  range   0-based indices of sample dimensions to read, or {@code 
null} or an empty sequence for reading them all.
@@ -331,24 +330,7 @@ dimensions: for (int j=0; j<tgtDim; j++) {
         if (domain == null) {
             domain = resource.getGridGeometry();
         }
-        return resource.read(getReadDomain(domain), readRanges);
-    }
-
-    /**
-     * Given a {@code GridGeometry} configured with the resolution to read, 
returns an amended domain.
-     * The default implementation returns {@code domain} unchanged.
-     * Subclasses can override typically for selecting a two-dimensional slice.
-     *
-     * <p>This method is invoked by {@link #getOrLoad(int)} default 
implementation before to read a coverage.</p>
-     *
-     * @param  domain  a grid geometry with the desired resolution.
-     * @return the domain to read from the {@linkplain #resource}.
-     *
-     * @see GridDerivation#slice(DirectPosition)
-     * @see GridDerivation#sliceByRatio(double, int...)
-     */
-    protected GridGeometry getReadDomain(final GridGeometry domain) {
-        return domain;
+        return resource.read(domain, readRanges);
     }
 
     /**
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index 1ee35693cc..81ffbcdec8 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -19,6 +19,7 @@ package org.apache.sis.internal.map.coverage;
 import java.util.Map;
 import java.util.List;
 import java.util.HashMap;
+import java.util.Objects;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.awt.Graphics2D;
@@ -42,8 +43,8 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.ImageRenderer;
-import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.PixelTranslation;
+import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
@@ -65,6 +66,7 @@ import 
org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.portrayal.PlanarCanvas;       // For javadoc.
@@ -75,6 +77,8 @@ import static java.util.logging.Logger.getLogger;
 /**
  * The {@code RenderedImage} to draw in a {@link PlanarCanvas} together with 
transforms from pixel coordinates
  * to display coordinates. This is a helper class for implementations of 
stateful renderer.
+ * All grid geometries and transforms managed by this class are 
two-dimensional.
+ * If the source data have more dimensions, a two-dimensional slice will be 
taken.
  *
  * <h2>Note on Java2D optimizations</h2>
  * {@link Graphics2D#drawRenderedImage(RenderedImage, AffineTransform)} 
implementation
@@ -100,13 +104,15 @@ import static java.util.logging.Logger.getLogger;
  * We wait to see if this class works well in the general case before doing 
special cases.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
 public class RenderingData implements Cloneable {
     /**
      * The {@value} value, for identifying code that assume two-dimensional 
objects.
+     *
+     * @see #xyDimensions
      */
     private static final int BIDIMENSIONAL = 2;
 
@@ -119,8 +125,8 @@ public class RenderingData implements Cloneable {
     private static final boolean CREATE_INDEX_COLOR_MODEL = true;
 
     /**
-     * Loader for reading and caching coverages at various resolutions. Used 
if no image has been
-     * explicitly assigned to {@link #data}, or if the image may vary 
depending on the resolution.
+     * Loader for reading and caching coverages at various resolutions.
+     * Required if no image has been explicitly assigned to {@link #data}.
      * The same instance may be shared by many {@link RenderingData} objects.
      */
     public MultiResolutionCoverageLoader coverageLoader;
@@ -131,7 +137,20 @@ public class RenderingData implements Cloneable {
     private int currentPyramidLevel;
 
     /**
-     * The data fetched from {@link GridCoverage#render(GridExtent)} for 
current {@code sliceExtent}.
+     * The slice extent which has been used for rendering the {@linkplain 
#data}.
+     * May be {@code null} if the grid coverage has only two dimensions with a 
size greater than 1 cell.
+     */
+    private GridExtent currentSlice;
+
+    /**
+     * The dimensions to select in the grid coverage for producing an image.
+     * This is an array of length {@value #BIDIMENSIONAL} almost always equal 
to {0,1}.
+     * The values are inferred from {@link #currentSlice}.
+     */
+    private int[] xyDimensions;
+
+    /**
+     * The data fetched from {@link GridCoverage#render(GridExtent)} for 
{@link #currentSlice}.
      * This rendered image may be tiled and fetching those tiles may require 
computations to be performed
      * in background threads. Pixels in this {@code data} image are mapped to 
pixels in the display
      * {@link PlanarCanvas} by the following chain of operations:
@@ -146,8 +165,7 @@ public class RenderingData implements Cloneable {
      *
      * @see #dataGeometry
      * @see #dataRanges
-     * @see #isEmpty()
-     * @see #loadIfNeeded(LinearTransform, DirectPosition, GridExtent)
+     * @see #ensureImageLoaded(GridCoverage, GridExtent, boolean)
      * @see #getSourceImage()
      */
     private RenderedImage data;
@@ -161,7 +179,7 @@ public class RenderingData implements Cloneable {
      *
      * @see #data
      * @see #dataRanges
-     * @see #setCoverageSpace(GridGeometry, List)
+     * @see #setImageSpace(GridGeometry, List, int[])
      */
     private GridGeometry dataGeometry;
 
@@ -169,7 +187,7 @@ public class RenderingData implements Cloneable {
      * Ranges of sample values in each band of {@link #data}. This is used for 
determining on which sample values
      * to apply colors when user asked to apply a color ramp. May be {@code 
null}.
      *
-     * @see #setCoverageSpace(GridGeometry, List)
+     * @see #setImageSpace(GridGeometry, List, int[])
      * @see #statistics()
      */
     private List<SampleDimension> dataRanges;
@@ -243,6 +261,8 @@ public class RenderingData implements Cloneable {
         data               = null;
         dataRanges         = null;
         dataGeometry       = null;
+        xyDimensions       = null;
+        currentSlice       = null;
     }
 
     /**
@@ -270,19 +290,22 @@ public class RenderingData implements Cloneable {
     }
 
     /**
-     * Sets the input space (domain) and output space (ranges) of the coverage 
to be rendered.
-     * It should be followed by a call to {@link 
#ensureImageLoaded(GridCoverage, GridExtent)}.
+     * Sets the input space (domain) and output space (ranges) of the image to 
be rendered.
+     * Those values can be initially provided by {@link 
org.apache.sis.storage.GridCoverageResource}
+     * and replaced later by the actual {@link GridCoverage} values after 
coverage loading is completed.
+     * It is caller's responsibility to reduce <var>n</var>-dimensional domain 
to two dimensions.
      *
      * @param  domain  the two-dimensional grid geometry, or {@code null} if 
there is no data.
      * @param  ranges  descriptions of bands, or {@code null} if there is no 
data.
-     *
-     * @see #isEmpty()
+     * @param  xyDims  the dimensions to select in the grid coverage for 
producing an image.
+     *                 This is an array of length {@value #BIDIMENSIONAL} 
almost always equal to {0,1}.
      */
     @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
-    public final void setCoverageSpace(final GridGeometry domain, final 
List<SampleDimension> ranges) {
+    public final void setImageSpace(final GridGeometry domain, final 
List<SampleDimension> ranges, final int[] xyDims) {
         processor.setFillValues(SampleDimensions.backgrounds(ranges));
         dataRanges   = ranges;      // Not cloned because already an 
unmodifiable list.
         dataGeometry = domain;
+        xyDimensions = xyDims;
         /*
          * If the grid geometry does not define a "grid to CRS" transform, set 
it to an identity transform.
          * We do that because this class needs a complete `GridGeometry` as 
much as possible.
@@ -305,7 +328,7 @@ public class RenderingData implements Cloneable {
      * It is caller's responsibility to ensure that {@link #coverageLoader} 
has a non-null value
      * and is using the right resource before to invoke this method.
      *
-     * <p>Caller should invoke {@link #ensureImageLoaded(GridCoverage, 
GridExtent)}
+     * <p>Caller should invoke {@link #ensureImageLoaded(GridCoverage, 
GridExtent, boolean)}
      * after this method (this is not done automatically).</p>
      *
      * @param  objectiveToDisplay  transform used for rendering the coverage 
on screen.
@@ -315,37 +338,40 @@ public class RenderingData implements Cloneable {
      * @throws TransformException if an error occurred while computing 
resolution from given transforms.
      * @throws DataStoreException if an error occurred while loading the 
coverage.
      *
-     * @see #setCoverageSpace(GridGeometry, List)
+     * @see #setImageSpace(GridGeometry, List, int[])
      */
     public final GridCoverage ensureCoverageLoaded(final LinearTransform 
objectiveToDisplay, final DirectPosition objectivePOI)
             throws TransformException, DataStoreException
     {
         final MathTransform dataToObjective = (changeOfCRS != null) ? 
changeOfCRS.getMathTransform() : null;
-        final int level = coverageLoader.findPyramidLevel(dataToObjective, 
objectiveToDisplay, objectivePOI);
+        final MultiResolutionCoverageLoader loader = coverageLoader;
+        final int level = loader.findPyramidLevel(dataToObjective, 
objectiveToDisplay, objectivePOI);
         if (data != null && level == currentPyramidLevel) {
             return null;
         }
         data = null;
         currentPyramidLevel = level;
-        return coverageLoader.getOrLoad(level);
+        return loader.getOrLoad(level);
     }
 
     /**
-     * Fetches the rendered image if {@linkplain #data} is null. This method 
needs to be invoked at least
-     * once after {@link #setCoverageSpace(GridGeometry, List)}. The {@code 
coverage} given in argument
-     * may be the value returned by {@link 
#ensureCoverageLoaded(LinearTransform, DirectPosition)}.
+     * Fetches the rendered image if {@linkplain #data} is null or is for a 
different slice.
+     * This method needs to be invoked at least once after {@link 
#setImageSpace(GridGeometry, List, int[])}.
+     * The {@code coverage} given in argument should be the value returned by 
a previous call to
+     * {@link #ensureCoverageLoaded(LinearTransform, DirectPosition)}, except 
that it shall not be null.
      *
-     * @param  coverage     the coverage from which to read data, or {@code 
null} if the coverage did not changed.
+     * @param  coverage     the coverage from which to read data. Shall not be 
null.
      * @param  sliceExtent  a subspace of the grid coverage extent where all 
dimensions except two have a size of 1 cell.
-     *         May be {@code null} if this grid coverage has only two 
dimensions with a size greater than 1 cell.
+     *                      May be {@code null} if this grid coverage has only 
two dimensions with a size greater than 1 cell.
+     * @param  force        whether to force data loading. Should be {@code 
true} if {@code coverage} changed since last call.
      * @return whether the {@linkpalin #data} changed.
      * @throws FactoryException if the CRS changed but the transform from old 
to new CRS can not be determined.
      * @throws TransformException if an error occurred while transforming 
coordinates from old to new CRS.
      */
-    public final boolean ensureImageLoaded(GridCoverage coverage, final 
GridExtent sliceExtent)
+    public final boolean ensureImageLoaded(GridCoverage coverage, final 
GridExtent sliceExtent, final boolean force)
             throws FactoryException, TransformException
     {
-        if (data != null || coverage == null) {
+        if (!force && data != null && Objects.equals(currentSlice, 
sliceExtent)) {
             return false;
         }
         coverage = coverage.forConvertedValues(true);
@@ -353,9 +379,19 @@ public class RenderingData implements Cloneable {
         final List<SampleDimension> ranges = coverage.getSampleDimensions();
         final RenderedImage image = coverage.render(sliceExtent);
         final Object value = image.getProperty(PlanarImage.GRID_GEOMETRY_KEY);
-        final GridGeometry domain = (value instanceof GridGeometry) ? 
(GridGeometry) value
-                : new ImageRenderer(coverage, 
sliceExtent).getImageGeometry(BIDIMENSIONAL);
-        setCoverageSpace(domain, ranges);
+        final GridGeometry domain;
+        final int[] xyDims;
+        if (value instanceof GridGeometry) {
+            domain = (GridGeometry) value;
+            xyDims = (sliceExtent == null) ? ArraysExt.range(0, BIDIMENSIONAL)
+                     : sliceExtent.getSubspaceDimensions(BIDIMENSIONAL);
+        } else {
+            ImageRenderer r = new ImageRenderer(coverage, sliceExtent);
+            domain = r.getImageGeometry(BIDIMENSIONAL);
+            xyDims = r.getXYDimensions();
+        }
+        setImageSpace(domain, ranges, xyDims);
+        currentSlice = sliceExtent;
         data = image;
         /*
          * Update the transforms in a way that preserve the current zoom 
level, translation, etc.
@@ -412,7 +448,7 @@ public class RenderingData implements Cloneable {
     /**
      * Returns the image which will be used as the source for rendering 
operations.
      *
-     * @return the image loaded be {@link #ensureImageLoaded(GridCoverage, 
GridExtent)}.
+     * @return the image loaded be {@link #ensureImageLoaded(GridCoverage, 
GridExtent, boolean)}.
      */
     public final RenderedImage getSourceImage() {
         return data;
@@ -453,10 +489,27 @@ public class RenderingData implements Cloneable {
     protected final Map<String,Object> statistics() throws DataStoreException {
         if (statistics == null) {
             RenderedImage image = data;
-            if (coverageLoader != null) {
-                final int level = coverageLoader.getLastLevel();
+            final MultiResolutionCoverageLoader loader = coverageLoader;
+            if (loader != null) {
+                final int level = loader.getLastLevel();
                 if (level != currentPyramidLevel) {
-                    image = 
coverageLoader.getOrLoad(level).forConvertedValues(true).render(null);
+                    /*
+                     * If coarser data are available, we will compute 
statistics on those data instead of on the
+                     * current pyramid level. We need to adjust the slice 
extent to the coordinates of coarser data.
+                     */
+                    final GridCoverage coarse = 
loader.getOrLoad(level).forConvertedValues(true);
+                    GridExtent sliceExtent = currentSlice;
+                    if (sliceExtent != null) {
+                        if (sliceExtent.getDimension() <= BIDIMENSIONAL) {
+                            sliceExtent = null;
+                        } else {
+                            final GridExtent ce = 
coarse.getGridGeometry().getExtent();
+                            for (final int i : xyDimensions) {
+                                sliceExtent = sliceExtent.withRange(i, 
ce.getLow(i), ce.getHigh(i));
+                            }
+                        }
+                    }
+                    image = coarse.render(sliceExtent);
                 }
             }
             statistics = processor.valueOfStatistics(image, null, 
SampleDimensions.toSampleFilters(processor, dataRanges));
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java
index ab1e6286be..c854222da7 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java
@@ -23,7 +23,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
index 8acf005185..4240e1be35 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
@@ -28,6 +28,9 @@ import org.opengis.geometry.MismatchedDimensionException;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.util.resources.Errors;
 
+// Branch-specific dependencies
+import org.opengis.coverage.grid.GridEnvelope;
+
 
 /**
  * Static methods for performing argument checks.
@@ -84,7 +87,7 @@ import org.apache.sis.util.resources.Errors;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -873,6 +876,30 @@ public final class ArgumentChecks extends Static {
         }
     }
 
+    /**
+     * Ensures that the given grid envelope, if non-null, has the expected 
number of dimensions.
+     * This method does nothing if the given grid envelope is null.
+     *
+     * @param  name      the name of the argument to be checked. Used only if 
an exception is thrown.
+     * @param  expected  the expected number of dimensions.
+     * @param  envelope  the grid envelope to check for its dimension, or 
{@code null}.
+     * @throws MismatchedDimensionException if the given envelope is non-null 
and does
+     *         not have the expected number of dimensions.
+     *
+     * @since 1.3
+     */
+    public static void ensureDimensionMatches(final String name, final int 
expected, final GridEnvelope envelope)
+            throws MismatchedDimensionException
+    {
+        if (envelope != null) {
+            final int dimension = envelope.getDimension();
+            if (dimension != expected) {
+                throw new MismatchedDimensionException(Errors.format(
+                        Errors.Keys.MismatchedDimension_3, name, expected, 
dimension));
+            }
+        }
+    }
+
     /**
      * Ensures that the given transform, if non-null, has the expected number 
of source and target dimensions.
      * This method does nothing if the given transform is null.

Reply via email to