You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2020/02/22 17:04:11 UTC

[sis] branch geoapi-4.0 updated (c46f427 -> 2d4382a)

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

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


    from c46f427  Workaround a bug of BufferedImage.getTileGridXOffset() / getTileGridYOffset() not returning zero (contrarily to what their javadoc said).
     new e819438  Add reference to bug https://bugs.openjdk.java.net/browse/JDK-8166038
     new 89b6293  Consolidation of the we relate "tabular data window" and "visualization window".
     new 149e764  Fix a NullPointerException when showing the FeatureTable window before the corresponding tab in the overview window has been made visible.
     new 8ade352  Try again to fix the problem of missing column headers. This hack seems to work.
     new 7193100  First draft of a CoverageView window showing the image (identity transform only for now).
     new 2d4382a  Add transparency support in images backed by ScaledColorSpace.

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  59 ++++++--
 .../org/apache/sis/gui/coverage/CoverageView.java  | 113 ++++++++++++++-
 .../org/apache/sis/gui/coverage/GridViewSkin.java  |  11 +-
 .../org/apache/sis/gui/dataset/DataWindow.java     | 125 +++++++++-------
 .../org/apache/sis/gui/dataset/FeatureTable.java   |  45 ++++--
 .../org/apache/sis/internal/gui/Resources.java     |  10 ++
 .../apache/sis/internal/gui/Resources.properties   |   2 +
 .../sis/internal/gui/Resources_fr.properties       |   2 +
 .../org/apache/sis/internal/gui/ToolbarButton.java | 120 ++++++++++++++--
 .../java/org/apache/sis/image/PixelIterator.java   |   2 +
 .../internal/coverage/j2d/ColorModelFactory.java   |  10 +-
 .../internal/coverage/j2d/ScaledColorModel.java    | 159 +++++++++++++++++++++
 .../internal/coverage/j2d/ScaledColorSpace.java    |  13 +-
 13 files changed, 565 insertions(+), 106 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java


[sis] 01/06: Add reference to bug https://bugs.openjdk.java.net/browse/JDK-8166038

Posted by de...@apache.org.
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 e819438019aedac5765d16d4b5bc6b57a2e16c06
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Feb 21 15:58:55 2020 +0100

    Add reference to bug https://bugs.openjdk.java.net/browse/JDK-8166038
---
 core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
index 9732efc..10564d7 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -318,6 +318,8 @@ public abstract class PixelIterator {
              * one tile at index (0,0).  But they return `raster.getSampleModelTranslateX()` instead, which may
              * be non-zero if the image is a sub-region of another image.  Delegating to `create(Raster)` avoid
              * this problem in addition of being a slight optimization.
+             *
+             * Issue tracker: https://bugs.openjdk.java.net/browse/JDK-8166038
              */
             if (order == SequenceType.LINEAR) {
                 return new LinearIterator(data, null, subArea, window);


[sis] 06/06: Add transparency support in images backed by ScaledColorSpace.

Posted by de...@apache.org.
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 2d4382ae5e52ab880961a9fc158773642e2851a5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Feb 22 18:03:17 2020 +0100

    Add transparency support in images backed by ScaledColorSpace.
---
 .../internal/coverage/j2d/ColorModelFactory.java   |  10 +-
 .../internal/coverage/j2d/ScaledColorModel.java    | 159 +++++++++++++++++++++
 .../internal/coverage/j2d/ScaledColorSpace.java    |  13 +-
 3 files changed, 174 insertions(+), 8 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
index cae70b0..90d0296 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
@@ -100,7 +100,7 @@ public final class ColorModelFactory {
                                        r2.getKey().getMinDouble(true));
 
     /**
-     * The minimum and maximum sample values.
+     * The minimum (inclusive) and maximum (exclusive) sample values.
      */
     private final double minimum, maximum;
 
@@ -228,7 +228,13 @@ public final class ColorModelFactory {
          */
         if (type != DataBuffer.TYPE_BYTE && type != DataBuffer.TYPE_USHORT) {
             final ColorSpace colors = createColorSpace(numBands, visibleBand, minimum, maximum);
-            return unique(new ComponentColorModel(colors, false, false, Transparency.OPAQUE, type));
+            final ComponentColorModel cm;
+            if (colors instanceof ScaledColorSpace) {
+                cm = new ScaledColorModel((ScaledColorSpace) colors, minimum, maximum, type);
+            } else {
+                cm = new ComponentColorModel(colors, false, false, Transparency.OPAQUE, type);
+            }
+            return unique(cm);
         }
         /*
          * If there is no category, constructs a gray scale palette.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java
new file mode 100644
index 0000000..e63c920
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.awt.Transparency;
+import java.awt.image.DataBuffer;
+import java.awt.image.ComponentColorModel;
+import java.awt.image.RasterFormatException;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.internal.feature.Resources;
+
+
+/**
+ * A color model for use with {@link ScaledColorSpace} (gray scale image with missing values).
+ * This color model is slightly more efficient than the default {@link ComponentColorModel} by
+ * reducing the amount of object allocations, made possible by the knowledge that we use only
+ * one sample value and returns only one color component (the gray).
+ * In addition, this class render the {@link Float#NaN} values as transparent.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class ScaledColorModel extends ComponentColorModel {
+    /**
+     * Index of the band to display. This is a copy of {@link ScaledColorSpace#visibleBand}.
+     */
+    private final int visibleBand;
+
+    /**
+     * The scaling factor from sample values to RGB normalized values.
+     * This information duplicates {@link ScaledColorSpace#scale} but
+     * for a scale from 0 to 256 instead than 0 to 1.
+     */
+    private final double scale;
+
+    /**
+     * The offset to subtract from sample values before to apply the {@linkplain #scale} factor.
+     * This information duplicates {@link ScaledColorSpace#offset} but for a scale from 0 to 256
+     * instead than 0 to 1.
+     */
+    private final double offset;
+
+    /**
+     * Creates a new color model.
+     *
+     * @param  colorSpace  the color space to use with this color model.
+     * @param  minimum     the minimal sample value expected, inclusive.
+     * @param  maximum     the maximal sample value expected, exclusive.
+     * @param  type        one of the {@link DataBuffer} constants.
+     */
+    ScaledColorModel(final ScaledColorSpace colorSpace, final double minimum, final double maximum, final int type) {
+        super(colorSpace, false, false, Transparency.BITMASK, type);
+        visibleBand = colorSpace.visibleBand;
+        scale  = 0x100 / (maximum - minimum);
+        offset = minimum;
+    }
+
+    /**
+     * Returns the red component of the given value.
+     * Defined for consistency but should not be used.
+     */
+    @Override
+    public int getRed(final Object inData) {
+        return getRGB(inData) & 0xFF;
+    }
+
+    /**
+     * Returns the green component of the given value.
+     * Defined for consistency but should not be used.
+     */
+    @Override
+    public int getGreen(final Object inData) {
+        return (getRGB(inData) >>> Byte.SIZE) & 0xFF;
+    }
+
+    /**
+     * Returns the green component of the given value.
+     * Defined for consistency but should not be used.
+     */
+    @Override
+    public int getBlue(final Object inData) {
+        return (getRGB(inData) >>> 2*Byte.SIZE) & 0xFF;
+    }
+
+    /**
+     * Returns the alpha value for the given sample values.
+     * This is based only on whether or not the value is NaN.
+     */
+    @Override
+    public int getAlpha(final Object inData) {
+        switch (transferType) {
+            case DataBuffer.TYPE_FLOAT:  return Float .isNaN(((float[])  inData)[visibleBand]) ? 0 : 0xFF;
+            case DataBuffer.TYPE_DOUBLE: return Double.isNaN(((double[]) inData)[visibleBand]) ? 0 : 0xFF;
+            default: return 0xFF;
+        }
+    }
+
+    /**
+     * Returns the color/alpha components for the specified pixel in the default RGB color model format.
+     */
+    @Override
+    public int getRGB(final Object inData) {
+        final double value;
+        switch (transferType) {
+            case DataBuffer.TYPE_BYTE:   value = Byte .toUnsignedInt(((byte[])   inData)[visibleBand]); break;
+            case DataBuffer.TYPE_USHORT: value = Short.toUnsignedInt(((short[])  inData)[visibleBand]); break;
+            case DataBuffer.TYPE_SHORT:  value =                     ((short[])  inData)[visibleBand];  break;
+            case DataBuffer.TYPE_INT:    value =                     ((int[])    inData)[visibleBand];  break;
+            case DataBuffer.TYPE_FLOAT:  value =                     ((float[])  inData)[visibleBand];  break;
+            case DataBuffer.TYPE_DOUBLE: value =                     ((double[]) inData)[visibleBand];  break;
+            default: throw new RasterFormatException(Resources.format(Resources.Keys.UnknownDataType_1, transferType));
+        }
+        if (Double.isNaN(value)) {
+            return 0;                                           // Transparent pixel.
+        }
+        final int c = Math.max(0, Math.min(0xFF, (int) ((value - offset) * scale)));
+        return c | (c << Byte.SIZE) | (c << 2*Byte.SIZE) | 0xFF000000;
+    }
+
+    /**
+     * Returns a hash code value for this color model.
+     */
+    @Override
+    public int hashCode() {
+        return Long.hashCode(Double.doubleToLongBits(scale)
+                      + 31 * Double.doubleToLongBits(offset))
+                      +  7 * getNumComponents() + visibleBand;
+    }
+
+    /**
+     * Compares this color model with the given object for equality.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other instanceof ScaledColorModel && super.equals(other)) {
+            final ScaledColorModel that = (ScaledColorModel) other;
+            return visibleBand == that.visibleBand
+                    && Numerics.equals(scale,  that.scale)
+                    && Numerics.equals(offset, that.offset);
+        }
+        return false;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorSpace.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorSpace.java
index 0e97622..9722fb4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorSpace.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorSpace.java
@@ -25,7 +25,7 @@ import org.apache.sis.util.collection.WeakHashSet;
 /**
  * Color space for images storing pixels as real numbers. The color space can have an
  * arbitrary number of bands, but in current implementation only one band is used.
- * Current implementation create a gray scale.
+ * Current implementation creates a gray scale.
  *
  * <p>The use of this color space is very slow.
  * It should be used only when no standard color space can be used.</p>
@@ -33,6 +33,7 @@ import org.apache.sis.util.collection.WeakHashSet;
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @version 1.1
  *
+ * @see ScaledColorModel
  * @see ColorModelFactory#createColorSpace(int, int, double, double)
  *
  * @since 1.0
@@ -72,7 +73,7 @@ final class ScaledColorSpace extends ColorSpace {
     /**
      * Index of the band to display.
      */
-    private final int visibleBand;
+    final int visibleBand;
 
     /**
      * Creates a color model for the given range of values.
@@ -80,8 +81,8 @@ final class ScaledColorSpace extends ColorSpace {
      *
      * @param  numComponents  the number of components.
      * @param  visibleBand    the band to use for computing colors.
-     * @param  minimum        the minimal sample value expected.
-     * @param  maximum        the maximal sample value expected.
+     * @param  minimum        the minimal sample value expected, inclusive.
+     * @param  maximum        the maximal sample value expected, exclusive.
      */
     ScaledColorSpace(final int numComponents, final int visibleBand, final double minimum, final double maximum) {
         super(TYPE_GRAY, numComponents);
@@ -171,7 +172,7 @@ final class ScaledColorSpace extends ColorSpace {
     }
 
     /**
-     * Returns a string representation of this color model.
+     * Returns a string representation of this color space.
      *
      * @return a string representation for debugging purpose.
      */
@@ -203,7 +204,7 @@ final class ScaledColorSpace extends ColorSpace {
     }
 
     /**
-     * Returns a hash code value for this color model.
+     * Returns a hash code value for this color space.
      * Defined for implementation of {@link #unique()}.
      */
     @Override


[sis] 04/06: Try again to fix the problem of missing column headers. This hack seems to work.

Posted by de...@apache.org.
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 8ade352ae5795581fbbaa86ee2f0f2cc93ff7810
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Feb 22 14:09:56 2020 +0100

    Try again to fix the problem of missing column headers. This hack seems to work.
---
 .../main/java/org/apache/sis/gui/coverage/GridViewSkin.java   | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

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 5fdf06a..a1323c7 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
@@ -510,8 +510,17 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme
                 layoutInArea(cell, pos, y, cellWidth, headerHeight, Node.BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.RIGHT, VPos.CENTER);
                 pos += cellWidth;
             }
+            /*
+             * For a mysterious reason, all row headers except the first one (0) are invisible on the first time
+             * that the grid is shown. I have been unable to identify the reason; all `GridCell` are created and
+             * received a non-empty text string. Doing a full layout again makes them appear. So as a workaround
+             * we request the next layout to be full again if it seems that we have done the initial layout. The
+             * very first layout create one cell (count = 0 & missing = 1), the next layout create missing cells
+             * (count = 1 & missing = 18) — this is where we want to force a third layou — then the third layout
+             * is stable (count = 19 & missing = 0).
+             */
+            layoutAll = count <= missing;
         }
-        layoutAll = false;
         if (hasErrors) {
             computeErrorBounds(flow);
         }


[sis] 03/06: Fix a NullPointerException when showing the FeatureTable window before the corresponding tab in the overview window has been made visible.

Posted by de...@apache.org.
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 149e7647aa06fbf8882672423fd68a9924cbb43c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Feb 22 13:18:29 2020 +0100

    Fix a NullPointerException when showing the FeatureTable window before the corresponding tab in the overview window has been made visible.
---
 .../org/apache/sis/gui/dataset/FeatureTable.java   | 45 +++++++++++++++++-----
 1 file changed, 35 insertions(+), 10 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
index 9902559..94c4a90 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
@@ -26,6 +26,8 @@ import javafx.scene.control.TableView;
 import javafx.scene.control.TableCell;
 import javafx.scene.layout.StackPane;
 import javafx.scene.layout.Region;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
 import javafx.beans.DefaultProperty;
 import javafx.beans.property.ReadOnlyObjectWrapper;
 import javafx.beans.property.SimpleObjectProperty;
@@ -140,8 +142,28 @@ public class FeatureTable extends TableView<Feature> {
         textLocale    = other.textLocale;
         dataLocale    = other.dataLocale;
         featureType   = other.featureType;
-        initialize();
-        createColumns(featureType);
+        setFeatures(other.getFeatures());           // Shall be invoked before to install the listener.
+        initialize();                               // Install listener.
+        if (featureType != null) {
+            createColumns();
+        } else if (getFeatures() != null) {
+            /*
+             * It may not be possible to create the columns immediately because the table is still loading
+             * in a background thread. In such case, we will create the columns later when the feature type
+             * will become known (which we identify by the other feature table updating its own columns).
+             */
+            other.getColumns().addListener(new InvalidationListener() {
+                @Override public void invalidated(final Observable list) {
+                    list.removeListener(this);                              // This event is needed only once.
+                    if (other.getFeatures() == getFeatures()) {
+                        featureType = other.featureType;
+                        if (featureType != null) {
+                            createColumns();
+                        }
+                    }
+                }
+            });
+        }
     }
 
     /**
@@ -241,19 +263,22 @@ public class FeatureTable extends TableView<Feature> {
     final void setFeatureType(final FeatureType type) {
         setPlaceholder(null);
         getItems().clear();
-        if (type != null && !type.equals(featureType)) {
-            createColumns(type);
+        final boolean update = (type != null) && !type.equals(featureType);
+        /*
+         * The feature type must be set before to invoke `createColumns(…)` because it is used not only
+         * by that method, but also by the listener registered in `FeatureTable(other)` constructor.
+         */
+        featureType = type;
+        if (update) {
+            createColumns();
         }
-        featureType = type;     // Set only after `createColumns(…)` succeeded.
     }
 
     /**
-     * Creates table columns for the specified feature type.
-     *
-     * @param  type  the feature type, typically a new type before {@link #featureType} is updated.
+     * Creates table columns for the current {@link #featureType}.
      */
-    private void createColumns(final FeatureType type) {
-        final Collection<? extends PropertyType> properties = type.getProperties(true);
+    private void createColumns() {
+        final Collection<? extends PropertyType> properties = featureType.getProperties(true);
         final List<TableColumn<Feature,?>> columns = new ArrayList<>(properties.size());
         final List<String> multiValued = new ArrayList<>(columns.size());
         for (final PropertyType pt : properties) {


[sis] 02/06: Consolidation of the we relate "tabular data window" and "visualization window".

Posted by de...@apache.org.
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 89b62936a49f64da05b497f8817434cfaee3387e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Feb 22 12:22:56 2020 +0100

    Consolidation of the we relate "tabular data window" and "visualization window".
---
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  24 ++--
 .../org/apache/sis/gui/dataset/DataWindow.java     | 125 ++++++++++++---------
 .../org/apache/sis/internal/gui/Resources.java     |  10 ++
 .../apache/sis/internal/gui/Resources.properties   |   2 +
 .../sis/internal/gui/Resources_fr.properties       |   2 +
 .../org/apache/sis/internal/gui/ToolbarButton.java | 120 +++++++++++++++++---
 6 files changed, 209 insertions(+), 74 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index a94993d..09bf287 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -26,6 +26,7 @@ import javafx.collections.ObservableList;
 import javafx.geometry.Insets;
 import javafx.scene.control.Accordion;
 import javafx.scene.control.Control;
+import javafx.scene.control.Button;
 import javafx.scene.control.Label;
 import javafx.scene.control.Slider;
 import javafx.scene.control.SplitPane;
@@ -41,6 +42,7 @@ import javafx.scene.layout.Region;
 import javafx.scene.layout.VBox;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.gui.ToolbarButton;
 import org.apache.sis.util.resources.Vocabulary;
@@ -125,7 +127,7 @@ public class CoverageExplorer {
         }
         /*
          * "Display" section with the following controls:
-         *    - Number format as a localized pattern (TODO).
+         *    - Number format as a localized pattern.
          *    - Cell width as a slider.
          */
         final VBox displayPane;
@@ -183,12 +185,20 @@ addRows:    for (int row = 0;; row++) {
          * they are managed by org.apache.sis.gui.dataset.DataWindow. We only declare here the
          * text and action for each button.
          */
-        content.getProperties().put(ToolbarButton.PROPERTY_KEY, new ToolbarButton[] {
-            new ToolbarButton() {
-                @Override public String getText() {return "\uD83D\uDDFA\uFE0F";}      // World map character.
-                @Override public Region createView() {
-                    return new CoverageView(null).getView();
-                }
+        ToolbarButton.insert(content, new ToolbarButton.RelatedWindow() {
+            /** 🗺 — World map. */
+            @Override public Button createButton(final Resources localized) {
+                return createButton("\uD83D\uDDFA\uFE0F", localized, Resources.Keys.Visualize);
+            }
+
+            /** 🔢 — Input symbol for numbers. */
+            @Override public Button createBackButton(final Resources localized) {
+                return createButton("\uD83D\uDD22\uFE0F", localized, Resources.Keys.TabularData);
+            }
+
+            /** Creates a visualization of the coverage. */
+            @Override public Region createView(final Locale locale) {
+                return new CoverageView(locale).getView();
             }
         });
     }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/DataWindow.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/DataWindow.java
index 40244cd..59ca2cc 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/DataWindow.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/DataWindow.java
@@ -46,25 +46,31 @@ import org.apache.sis.internal.gui.ToolbarButton;
  */
 final class DataWindow extends Stage {
     /**
-     * The locale for this window.
+     * The locale for texts and tooltips in this window.
      */
     private final Locale locale;
 
     /**
-     * The tools bar. Removed from the pane when going in full screen mode,
-     * and reinserted when exiting full screen mode.
+     * The tools bar. Removed from the pane when going in full screen mode, and reinserted
+     * when exiting full screen mode. The first button in this toolbar shall be the "home"
+     * button (for showing the main window on front) — if a different position is desired,
+     * revisit {@link #getHomeButton()}.
+     *
+     * @see #getHomeButton()
+     * @see #onFullScreen(boolean)
      */
     private final ToolBar tools;
 
     /**
      * Creates a new window for the given data selected in the explorer or determined by the active tab.
+     * The new window will be positioned in the screen center but not yet shown.
      *
      * @param  home  the window containing the main explorer, to be the target of "home" button.
      * @param  data  the data selected by user, to show in a new window.
      */
     DataWindow(final Stage home, final SelectedData data) {
-        this(null, data.createView(), data.localized,
-                (event) -> {home.show(); home.toFront();});
+        this(data.createView(), data.localized);
+        getHomeButton().setOnAction((e) -> {home.show(); home.toFront();});
         /*
          * We use an initial size covering a large fraction of the screen because
          * this window is typically used for showing image or large tabular data.
@@ -75,57 +81,54 @@ final class DataWindow extends Stage {
     }
 
     /**
-     * Creates a new window for the given content. The {@code home} and {@code localized} arguments
-     * shall be non-null only if {@code originator} is null.
+     * Returns the "home" button. This implementation assumes that "home" is the first button on the toolbar.
+     * This method is kept close to the following constructor for making easier to verify its assumption.
+     */
+    private Button getHomeButton() {
+        return ((Button) tools.getItems().get(0));
+    }
+
+    /**
+     * Creates a new window for the given content. After this constructor returned,
+     * caller should set an action on {@link #getHomeButton()} and set the window size.
      *
-     * @param  originator  the window from which this window is derived, or {@code null} if none.
-     * @param  content     content of the window to create.
-     * @param  localized   {@link Resources} instance provided because often know by the caller.
-     * @param  home        the action to execute when user clicks on the "home" button.
+     * @param  content    content of the window to create.
+     * @param  localized  {@link Resources} instance, provided because often known by the caller.
      */
-    private DataWindow(final DataWindow originator, final Region content, Resources localized, EventHandler<ActionEvent> home) {
-        if (originator != null) {
-            home = ((Button) originator.tools.getItems().get(0)).getOnAction();
-            localized = Resources.forLocale(originator.locale);
-        }
+    private DataWindow(final Region content, final Resources localized) {
         locale = localized.getLocale();
         /*
-         * Build the tools bar. This bar will be hidden in full screen mode.
-         * Note that code above assumes that this button is the first button in the toolbar.
+         * Build the tools bar. This bar will be hidden in full screen mode. Note that above
+         * method assumes that the "home" button created below is the first one in the toolbar.
          */
         final Button mainWindow = new Button("\u2302\uFE0F");               // ⌂ — house
         mainWindow.setTooltip(new Tooltip(localized.getString(Resources.Keys.MainWindow)));
-        mainWindow.setOnAction(home);
 
         final Button fullScreen = new Button("\u21F1\uFE0F");               // ⇱ — North West Arrow to Corner
         fullScreen.setTooltip(new Tooltip(localized.getString(Resources.Keys.FullScreen)));
         fullScreen.setOnAction((event) -> setFullScreen(true));
-        /*
-         * Hide/show the toolbar when entering/exiting full screen mode.
-         */
-        tools = new ToolBar(mainWindow, fullScreen);
         fullScreenProperty().addListener((source, oldValue, newValue) -> onFullScreen(newValue));
 
-        if (originator != null) {
-            final Button related = new Button("\uD83D\uDD22\uFE0F");    // Input symbol for numbers.
-            related.setOnAction((event) -> {originator.show(); originator.toFront();});
-            tools.getItems().add(related);
-        }
+        tools = new ToolBar(mainWindow, fullScreen);
         /*
          * Add content-specific buttons. We use the "org.apache.sis.gui.ToolbarButton" property
          * as a way to transfer ToolbarButton accross packages without making this class public.
          */
-        final ToolbarButton[] contentButtons = (ToolbarButton[]) content.getProperties().remove(ToolbarButton.PROPERTY_KEY);
-        if (contentButtons != null) {
-            for (final ToolbarButton tb : contentButtons) {
-                final Button b = new Button(tb.getText());
-                b.setOnAction(new Related(this, tb));
-                tools.getItems().add(b);
+        for (final ToolbarButton specialized : ToolbarButton.remove(content)) {
+            final Node button = specialized.createButton(localized);
+            if (specialized instanceof ToolbarButton.RelatedWindow) {
+                ((Button) button).setOnAction(new Related(this, (ToolbarButton.RelatedWindow) specialized));
             }
+            tools.getItems().add(button);
         }
-        final Font bf = Font.font(20);
+        /*
+         * After we finished adding all buttons, set the font of all of them to a larger size.
+         */
+        final Font font = Font.font(20);
         for (final Node node : tools.getItems()) {
-            ((Button) node).setFont(bf);
+            if (node instanceof Button) {
+                ((Button) node).setFont(font);
+            }
         }
         /*
          * Main content. After this constructor returned, caller
@@ -139,8 +142,8 @@ final class DataWindow extends Stage {
 
     /**
      * Manage the creation and display of another window related to the enclosing {@link DataWindow}.
-     * For example is the enclosing window shown the tabular data, the window created by this class
-     * may shown the map.
+     * For example is the enclosing window shows the tabular data, the window created by this class
+     * may show the map.
      */
     private static final class Related implements EventHandler<ActionEvent> {
         /**
@@ -149,41 +152,56 @@ final class DataWindow extends Stage {
         private static final int LOCATION = 40;
 
         /**
-         * The object that can create the window.
-         * This is set to {@code null} when no longer needed.
+         * The object for creating the window on the first time that the user clicks on the button.
+         * This is set to {@code null} when no longer needed, in which case {@link #window} should
+         * be a reference to the window that we created.
          */
-        private ToolbarButton creator;
+        private ToolbarButton.RelatedWindow creator;
 
         /**
          * The related window. If {@link #creator} is non-null, then this is the original window that
          * created this {@code Related} instance. If {@link #creator} is null, this is the new window
-         * that has been created.
+         * that this class has created.
          */
         private DataWindow window;
 
         /**
          * Prepares an action for invoking {@code creator.createView()} when first needed.
+         * The given {@code originator} window will be the target of the "back" button.
+         *
+         * @param  originator  the original window that created this {@code Related} instance.
+         * @param  creator     a factory for the new window to create when the user request it.
          */
-        Related(final DataWindow originator, final ToolbarButton creator) {
+        Related(final DataWindow originator, final ToolbarButton.RelatedWindow creator) {
             this.window  = originator;
             this.creator = creator;
         }
 
         /**
-         * Invoked when the user clicked on the button for showing the window managed by this {@code Related}.
-         * On the first click, the related window is created. On subsequent click, that window is brought to front.
+         * Invoked when the user clicked on the button for showing the window managed by this {@code Related} object.
+         * On the first click, the related window is created. On subsequent clicks, that window is brought to front.
          */
         @Override
         public void handle(final ActionEvent event) {
             if (creator != null) {
-                final String title = window.getTitle();     // TODO! make the title different.
-                final DataWindow rw = new DataWindow(window, creator.createView(), null, null);
-                rw.setTitle(title);
-                rw.setWidth (window.getWidth());
-                rw.setHeight(window.getHeight());
-                rw.setX(window.getX() + LOCATION);
-                rw.setY(window.getY() + LOCATION);
-                window  = rw;                   // Set only on success.
+                final DataWindow originator = window;
+                final Resources  localized  = Resources.forLocale(originator.locale);
+                final Region     content    = creator.createView(originator.locale);
+                final Button     backButton = creator.createBackButton(localized);
+                backButton.setOnAction((e) -> {originator.show(); originator.toFront();});
+                ToolbarButton.insert(content, new ToolbarButton() {
+                    @Override public Node createButton(Resources localized) {
+                        return backButton;
+                    }
+                });
+                final DataWindow rw = new DataWindow(content, localized);
+                rw.getHomeButton().setOnAction(originator.getHomeButton().getOnAction());
+                rw.setTitle (originator.getTitle());
+                rw.setWidth (originator.getWidth());
+                rw.setHeight(originator.getHeight());
+                rw.setX(originator.getX() + LOCATION);
+                rw.setY(originator.getY() + LOCATION);
+                window  = rw;                                       // Set only on success.
                 creator = null;
             }
             window.show();
@@ -193,6 +211,7 @@ final class DataWindow extends Stage {
 
     /**
      * Invoked when entering or existing the full screen mode.
+     * Used for hiding/showing the toolbar when entering/exiting full screen mode.
      */
     private void onFullScreen(final boolean entering) {
         final BorderPane pane = (BorderPane) getScene().getRoot();
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index 782474b..162ffad 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -291,6 +291,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short Summary = 29;
 
         /**
+         * Tabular data
+         */
+        public static final short TabularData = 51;
+
+        /**
          * Topic category:
          */
         public static final short TopicCategory = 25;
@@ -301,6 +306,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short TypeOfResource = 26;
 
         /**
+         * Visualize
+         */
+        public static final short Visualize = 52;
+
+        /**
          * Windows
          */
         public static final short Windows = 43;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index 581194d..e0dd468 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -67,6 +67,8 @@ SelectCRS              = Select a coordinate reference system
 SendTo                 = Send to
 StandardErrorStream    = Standard error stream
 Summary                = Summary
+TabularData            = Tabular data
 TopicCategory          = Topic category:
 TypeOfResource         = Type of resource:
+Visualize              = Visualize
 Windows                = Windows
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index de60cb6..ef7cf2a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -72,6 +72,8 @@ SelectCRS              = Choisir un syst\u00e8me de r\u00e9f\u00e9rence des coor
 SendTo                 = Envoyer vers
 StandardErrorStream    = Flux d\u2019erreur standard
 Summary                = R\u00e9sum\u00e9
+TabularData            = Tableau de valeurs
 TopicCategory          = Cat\u00e9gorie th\u00e9matique\u00a0:
 TypeOfResource         = Type de ressource\u00a0:
+Visualize              = Visualiser
 Windows                = Fen\u00eatres
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java
index 94a2384..d3f2109 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java
@@ -16,17 +16,23 @@
  */
 package org.apache.sis.internal.gui;
 
+import java.util.Locale;
 import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Tooltip;
 import javafx.scene.layout.Region;
+import org.apache.sis.util.ArraysExt;
 
 
 /**
- * A button in a the toolbar of a {@link org.apache.sis.gui.dataset.DataWindow},
- * other than the common buttons provided by {@code DataWindow} itself.
- * Those button depends on the window content.
+ * Description of a button to add in a the {@link org.apache.sis.gui.dataset.DataWindow} toolbar.
+ * This class is used only for content-specific buttons; it is not used for buttons managed directly by
+ * {@code DataWindow} itself. A {@code ToolbarButton} can create and configure a button with its icon,
+ * tooltip text and action to execute when the button is pushed. {@code ToolbarButton} instances exist
+ * only temporarily and are discarded after the button has been created and placed in the toolbar.
  *
- * <p>Current API is for creating a new window of related data. A future version
- * may move that API in a subclass if we need to support other kinds of service.</p>
+ * <p>This class is defined in this internal package for allowing interactions between classes
+ * in different packages without making toolbar API public.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -38,7 +44,40 @@ public abstract class ToolbarButton {
      * The property to use in {@link Node#getProperties()} for storing instances of this class.
      * Values associated to this key shall be arrays of {@code ToolbarButton[]} type.
      */
-    public static final String PROPERTY_KEY = "org.apache.sis.gui.ToolbarButton";
+    private static final String PROPERTY_KEY = "org.apache.sis.gui.ToolbarButton";
+
+    /**
+     * Gets and removes the toolbar buttons associated to the given content pane. Those buttons
+     * should have been specified by a previous call to {@link #insert(Node, ToolbarButton...)}.
+     * They will be requested by {@link org.apache.sis.gui.dataset.DataWindow} only once,
+     * which is why we remove them afterward.
+     *
+     * @param  content  the pane for which to get the toolbar buttons.
+     * @return the toolbar buttons (never null, but may be empty).
+     */
+    public static ToolbarButton[] remove(final Node content) {
+        final ToolbarButton[] buttons = (ToolbarButton[]) content.getProperties().remove(PROPERTY_KEY);
+        return (buttons != null) ? buttons : new ToolbarButton[0];
+    }
+
+    /**
+     * Sets the toolbar buttons that the given pane which to have in the data window.
+     * If the pane already has buttons, the new one will be inserted before existing ones.
+     *
+     * @param  content  the pane for which to set the toolbar buttons.
+     * @param  buttons  the toolbar buttons to add.
+     */
+    public static void insert(final Node content, final ToolbarButton... buttons) {
+        content.getProperties().merge(PROPERTY_KEY, buttons, ToolbarButton::prepend);
+    }
+
+    /**
+     * Invoked if toolbar buttons already exist for a pane, in which case the new ones
+     * are inserted before the existing ones.
+     */
+    private static Object prepend(final Object oldValue, final Object newValue) {
+        return ArraysExt.append((ToolbarButton[]) newValue, (ToolbarButton[]) oldValue);
+    }
 
     /**
      * For subclass constructors.
@@ -47,18 +86,71 @@ public abstract class ToolbarButton {
     }
 
     /**
-     * Returns the text to show in the button.
+     * Creates a button configured with its icon, tooltip and action.
+     * The button will be added to the toolbar by the caller.
+     *
+     * <p>If this {@code ToolbarButton} is an instance of {@link RelatedWindow},
+     * then this method does not need to set an action on the button because it
+     * will be done by the caller.</p>
      *
-     * @return the button text.
+     * @param  localized  an instance of {@link Resources} for current locale.
+     * @return the button configured with text or icon, tooltip and action.
      */
-    public abstract String getText();
+    public abstract Node createButton(Resources localized);
 
     /**
-     * Creates the content of the window to show when the user click on the button.
-     * This method is invoked only on the first click. For all subsequent clicks,
-     * the existing window will be shown again.
+     * Convenience method for creating a button.
      *
-     * @return content of the window to show.
+     * @param  icon       the text to put in the button, as a Unicode emoji.
+     * @param  localized  an instance of {@link Resources} for current locale.
+     * @param  tooltip    the {@link Resources.Keys} value for the tooltip.
+     * @return the button configured with text or icon, tooltip and action.
      */
-    public abstract Region createView();
+    protected static Button createButton(final String icon, final Resources localized, final short tooltip) {
+        final Button b = new Button(icon);
+        b.setTooltip(new Tooltip(localized.getString(tooltip)));
+        return b;
+    }
+
+    /**
+     * A toolbar button for creating and showing a new window related to the window in which the button has been pushed.
+     * The button action will create a new instance of {@link org.apache.sis.gui.dataset.DataWindow} which will itself
+     * contain a button for going back to the original window.
+     */
+    public abstract static class RelatedWindow extends ToolbarButton {
+        /**
+         * For subclass constructors.
+         */
+        protected RelatedWindow() {
+        }
+
+        /**
+         * Creates a button configured with its icon, tooltip and action.
+         * This button does not need to contain an action; it will be set by the caller.
+         *
+         * @param  localized  an instance of {@link Resources} for current locale.
+         * @return the button configured with text or icon and tooltip.
+         */
+        @Override
+        public abstract Button createButton(Resources localized);
+
+        /**
+         * Creates the button for navigation back to the original window.
+         * This button does not need to contain an action; it will be set by the caller.
+         *
+         * @param  localized  an instance of {@link Resources} for current locale.
+         * @return the button configured with text or icon and tooltip.
+         */
+        public abstract Button createBackButton(Resources localized);
+
+        /**
+         * Creates the content of the window to show when the user click on the button.
+         * This method is invoked only on the first click. For all subsequent clicks,
+         * the existing window will be shown again.
+         *
+         * @param  locale  locale of the window creating a new window.
+         * @return content of the window to show.
+         */
+        public abstract Region createView(Locale locale);
+    }
 }


[sis] 05/06: First draft of a CoverageView window showing the image (identity transform only for now).

Posted by de...@apache.org.
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 719310030f34cd0ace29dd3fb2f75eecff64caee
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Feb 22 15:32:32 2020 +0100

    First draft of a CoverageView window showing the image (identity transform only for now).
---
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  37 +++++--
 .../org/apache/sis/gui/coverage/CoverageView.java  | 113 +++++++++++++++++++--
 2 files changed, 136 insertions(+), 14 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index 09bf287..158c106 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -87,6 +87,12 @@ public class CoverageExplorer {
     public final ObjectProperty<GridCoverage> coverageProperty;
 
     /**
+     * Whether the {@link #coverageProperty} is in process of being set, in which case some
+     * listeners should not react.
+     */
+    private boolean isCoverageAdjusting;
+
+    /**
      * The component for showing sample values.
      */
     private final GridView gridView;
@@ -198,7 +204,9 @@ addRows:    for (int row = 0;; row++) {
 
             /** Creates a visualization of the coverage. */
             @Override public Region createView(final Locale locale) {
-                return new CoverageView(locale).getView();
+                final CoverageView view = new CoverageView(locale);
+                view.coverageProperty.bind(coverageProperty);
+                return view.getView();
             }
         });
     }
@@ -269,7 +277,7 @@ addRows:    for (int row = 0;; row++) {
     }
 
     /**
-     * Invoked when a new coverage has been specified
+     * Invoked when a new coverage has been specified.
      *
      * @param  property  the {@link #coverageProperty} (ignored).
      * @param  previous  ignored.
@@ -278,11 +286,13 @@ addRows:    for (int row = 0;; row++) {
     private void onCoverageSpecified(final ObservableValue<? extends GridCoverage> property,
                                      final GridCoverage previous, final GridCoverage coverage)
     {
-        gridView.setImage((RenderedImage) null);
-        if (coverage != null) {
-            gridView.setImage(new ImageRequest(coverage, null));        // Start a background thread.
+        if (!isCoverageAdjusting) {
+            gridView.setImage((RenderedImage) null);
+            setSampleDimensions(coverage);
+            if (coverage != null) {
+                gridView.setImage(new ImageRequest(coverage, null));        // Start a background thread.
+            }
         }
-        onCoverageLoaded(coverage);
     }
 
     /**
@@ -294,6 +304,19 @@ addRows:    for (int row = 0;; row++) {
      * @param  coverage  the new coverage, or {@code null} if loading failed.
      */
     final void onCoverageLoaded(final GridCoverage coverage) {
+        setSampleDimensions(coverage);
+        isCoverageAdjusting = true;
+        try {
+            setCoverage(coverage);
+        } finally {
+            isCoverageAdjusting = false;
+        }
+    }
+
+    /**
+     * Sets the values in the sample dimensions table according information in the given coverage.
+     */
+    private void setSampleDimensions(final GridCoverage coverage) {
         final ObservableList<SampleDimension> items = sampleDimensions.getItems();
         if (coverage != null) {
             items.setAll(coverage.getSampleDimensions());
@@ -304,7 +327,7 @@ addRows:    for (int row = 0;; row++) {
     }
 
     /**
-     * Invoked when the selected band changed. This method ensures that the selected row
+     * Invoked when the band property changed. This method ensures that the selected row
      * in the sample dimension table matches the band which is shown in the grid view.
      */
     private void onBandSpecified(final ObservableValue<? extends Number> property,
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
index 8ba4aec..2bcfb14 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
@@ -31,9 +31,13 @@ import javafx.scene.image.PixelFormat;
 import javafx.scene.image.WritableImage;
 import javafx.scene.layout.Pane;
 import javafx.scene.layout.Region;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.util.Callback;
 import org.opengis.referencing.datum.PixelInCell;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.internal.map.PlanarCanvas;
@@ -53,6 +57,28 @@ import org.apache.sis.internal.util.Numerics;
  */
 final class CoverageView extends PlanarCanvas {
     /**
+     * The data shown in this view. Note that setting this property to a non-null value may not
+     * modify the view content immediately. Instead, a background process will request the tiles.
+     *
+     * <p>Current implementation is restricted to {@link GridCoverage} instances, but a future
+     * implementation may generalize to {@link org.opengis.coverage.Coverage} instances.</p>
+     *
+     * @see #getCoverage()
+     * @see #setCoverage(GridCoverage)
+     */
+    public final ObjectProperty<GridCoverage> coverageProperty;
+
+    /**
+     * 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.
+     *
+     * @see #getSliceExtent()
+     * @see #setSliceExtent(GridExtent)
+     * @see GridCoverage#render(GridExtent)
+     */
+    public final ObjectProperty<GridExtent> sliceExtentProperty;
+
+    /**
      * The data to shown, or {@code null} if not yet specified. This image may be tiled,
      * and fetching tiles may require computations to be performed in background thread.
      * The size of this image is not necessarily {@link #buffer} or {@link #image} size.
@@ -104,12 +130,26 @@ final class CoverageView extends PlanarCanvas {
      */
     public CoverageView(final Locale locale) {
         super(locale);
+        coverageProperty    = new SimpleObjectProperty<>(this, "coverage");
+        sliceExtentProperty = new SimpleObjectProperty<>(this, "sliceExtent");
         dataToImage = new AffineTransform();
-        view        = new Pane();
-        image       = new ImageView();
+        view = new Pane() {
+            @Override protected void layoutChildren() {
+                super.layoutChildren();
+                repaint();
+            }
+        };
+        image = new ImageView();
         image.setPreserveRatio(true);
         view.getChildren().add(image);
-        view.setPrefSize(600, 400);
+        /*
+         * Do not set a preferred size, otherwise `repaint()` is invoked twice: once with the preferred size
+         * and once with the actual size of the parent window. Actually the `repaint()` method appears to be
+         * invoked twice anyway, but without preferred size the width appears to be 0, in which case nothing
+         * is repainted.
+         */
+        coverageProperty   .addListener(this::onImageSpecified);
+        sliceExtentProperty.addListener(this::onImageSpecified);
     }
 
     /**
@@ -123,13 +163,69 @@ final class CoverageView extends PlanarCanvas {
     }
 
     /**
-     * Sets the image to display.
+     * Returns the source of image for this viewer.
+     * This method, like all other methods in this class, shall be invoked from the JavaFX thread.
+     *
+     * @return the coverage shown in this explorer, or {@code null} if none.
+     *
+     * @see #coverageProperty
+     */
+    public final GridCoverage getCoverage() {
+        return coverageProperty.get();
+    }
+
+    /**
+     * Sets the coverage to show in this viewer.
+     * This method shall be invoked from JavaFX thread and returns immediately.
+     * The new data are loaded in a background thread and will appear after an
+     * undetermined amount of time.
+     *
+     * @param  coverage  the data to show in this viewer, or {@code null} if none.
+     *
+     * @see #coverageProperty
      */
-    private void setImage(final RenderedImage source) {
+    public final void setCoverage(final GridCoverage coverage) {
+        coverageProperty.set(coverage);
+    }
+
+    /**
+     * Returns a subspace of the grid coverage extent where all dimensions except two have a size of 1 cell.
+     *
+     * @return subspace of the grid coverage extent where all dimensions except two have a size of 1 cell.
+     *
+     * @see #sliceExtentProperty
+     * @see GridCoverage#render(GridExtent)
+     */
+    public final GridExtent getSliceExtent() {
+        return sliceExtentProperty.get();
+    }
+
+    /**
+     * Sets a subspace of the grid coverage extent where all dimensions except two have a size of 1 cell.
+     *
+     * @param  sliceExtent  subspace of the grid coverage extent where all dimensions except two have a size of 1 cell.
+     *
+     * @see #sliceExtentProperty
+     * @see GridCoverage#render(GridExtent)
+     */
+    public final void setSliceExtent(final GridExtent sliceExtent) {
+        sliceExtentProperty.set(sliceExtent);
+    }
+
+    /**
+     * Invoked when a new coverage has been specified or when the slice extent changed.
+     *
+     * @param  property  the {@link #coverageProperty} or {@link #sliceExtentProperty} (ignored).
+     * @param  previous  ignored.
+     * @param  value     ignored.
+     */
+    private void onImageSpecified(final ObservableValue<?> property, final Object previous, final Object value) {
         image.setImage(null);
+        data   = null;
         buffer = null;
-        data = source;
-        if (source != null) {
+        final GridCoverage coverage = getCoverage();
+        if (coverage != null) {
+            data = coverage.render(getSliceExtent());     // TODO: background thread.
             repaint();
         }
     }
@@ -142,6 +238,9 @@ final class CoverageView extends PlanarCanvas {
     private void repaint() {
         final int width  = Numerics.clamp(Math.round(view.getWidth()));
         final int height = Numerics.clamp(Math.round(view.getHeight()));
+        if (width <= 0 || height <= 0) {
+            return;
+        }
         PixelBuffer<IntBuffer> wrapper = bufferWrapper;
         BufferedImage drawTo = buffer;
         if (drawTo == null || drawTo.getWidth() != width || drawTo.getHeight() != height) {