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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 9adfa358f6 Add sliders for selecting the slice to show in a 3 (or 
more) dimensional data cube. Slider graduation is okay but selecting a value 
does not yet have an effect.
9adfa358f6 is described below

commit 9adfa358f690d6bfc6ac9f38ddec4df2f7234e06
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon May 23 17:26:54 2022 +0200

    Add sliders for selecting the slice to show in a 3 (or more) dimensional 
data cube.
    Slider graduation is okay but selecting a value does not yet have an effect.
---
 .../apache/sis/gui/coverage/CoverageControls.java  |   9 +-
 .../org/apache/sis/gui/coverage/GridControls.java  |  11 +-
 .../apache/sis/gui/coverage/GridSliceSelector.java | 474 +++++++++++++++++++++
 .../apache/sis/gui/coverage/ViewAndControls.java   |  53 ++-
 .../sis/gui/coverage/GridSliceSelectorApp.java     |  80 ++++
 .../main/java/org/apache/sis/referencing/CRS.java  |   3 +-
 .../sis/referencing/crs/DefaultTemporalCRS.java    |  48 ++-
 .../apache/sis/referencing/crs/package-info.java   |   2 +-
 .../referencing/crs/DefaultTemporalCRSTest.java    |  15 +-
 .../org/apache/sis/measure/AbstractConverter.java  |   3 +
 .../main/java/org/apache/sis/measure/Units.java    |   1 +
 .../sis/util/resources/IndexedResourceBundle.java  |  12 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 .../apache/sis/util/resources/package-info.java    |   2 +-
 16 files changed, 695 insertions(+), 25 deletions(-)

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 d8f24b426e..9168dec7f0 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
@@ -146,8 +146,8 @@ final class CoverageControls extends ViewAndControls {
          * `CoverageExplorer` properties. This constructor does not install 
listeners in the opposite
          * direction; instead `CoverageExplorer` will invoke 
`load(ImageRequest)`.
          */
-        view.resourceProperty.addListener((p,o,n) -> onPropertySet(n, null));
-        view.coverageProperty.addListener((p,o,n) -> 
onPropertySet(view.getResourceIfAdjusting(), n));
+        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);
     }
@@ -160,7 +160,8 @@ 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.
      */
-    private void onPropertySet(final GridCoverageResource resource, final 
GridCoverage coverage) {
+    @Override
+    final void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
         final ObservableList<Category> items = categoryTable.getItems();
         if (coverage == null) {
             items.clear();
@@ -168,7 +169,7 @@ 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());
         }
-        owner.notifyDataChanged(resource, coverage);
+        super.notifyDataChanged(resource, coverage);
     }
 
     /**
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 99dcd22d36..eac6982f0a 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
@@ -107,17 +107,18 @@ final class GridControls extends ViewAndControls {
      * dimensions when information become available. This method is invoked in 
JavaFX thread.
      *
      * @param  resource  the new source of coverage, or {@code null} if none.
-     * @param  data      the new coverage, or {@code null} if none.
+     * @param  coverage  the new coverage, or {@code null} if none.
      */
-    final void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage data) {
+    @Override
+    final void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
         final ObservableList<SampleDimension> items = 
sampleDimensions.getItems();
-        if (data != null) {
-            items.setAll(data.getSampleDimensions());
+        if (coverage != null) {
+            items.setAll(coverage.getSampleDimensions());
             
sampleDimensions.getSelectionModel().clearAndSelect(view.getBand());
         } else {
             items.clear();
         }
-        owner.notifyDataChanged(resource, data);
+        super.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
new file mode 100644
index 0000000000..8137bf0c81
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java
@@ -0,0 +1,474 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.coverage;
+
+import java.time.Duration;
+import java.text.DateFormat;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.logging.Logger;
+import javafx.scene.Node;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.control.Label;
+import javafx.scene.control.Slider;
+import javafx.geometry.VPos;
+import javafx.geometry.Insets;
+import javafx.util.StringConverter;
+import javafx.collections.ObservableList;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import org.opengis.geometry.Envelope;
+import org.opengis.util.FactoryException;
+import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.TemporalCRS;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.TransformSeparator;
+import org.apache.sis.referencing.crs.DefaultTemporalCRS;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.gui.Widget;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.util.iso.Types;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * A control for selecting a two-dimensional slice in a grid extent having 
more than 2 dimensions.
+ * For example if a <var>n</var>-dimensional data cube contains a time axis, 
this widget provides
+ * a slider for selecting a slice on the time axis.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public class GridSliceSelector extends Widget {
+    /**
+     * Constants used for identifying the code assuming a two-dimensional 
space.
+     */
+    private static final int BIDIMENSIONAL = 2;
+
+    /**
+     * Some spaces to add around the selectors.
+     */
+    private static final Insets PADDING = new Insets(3, 9, 3, 9);
+
+    /**
+     * Approximate number of pixels per character. Used for computing the 
estimated size of labels.
+     * This is okay if larger than reality, as it will just put more space 
between labels.
+     */
+    private static final int PIXELS_PER_CHAR = 10;
+
+    /**
+     * Maximum number of minor ticks to place between two major ticks, 
including one major tick.
+     * The actual number will be adjusted so that there is no tick smaller 
than grid cells.
+     */
+    private static final int MAX_MINOR_TICK_COUNT = 10;
+
+    /**
+     * The grid geometry for which to provide sliders.
+     */
+    public final ObjectProperty<GridGeometry> gridGeometry;
+
+    /**
+     * The locale to use for axis labels, or {@code null} for a default locale.
+     */
+    private final Locale locale;
+
+    /**
+     * The pane which contains all sliders. The grid has two columns and a 
number of rows
+     * equals to the number of {@link #sliders} to be shown. Children at even 
indices are
+     * labels and children at odd indices are sliders.
+     */
+    private final GridPane view;
+
+    /**
+     * The object to use for formatting numbers, created when first needed.
+     *
+     * @see #getNumberFormat()
+     */
+    private NumberFormat numberFormat;
+
+    /**
+     * The object to use for formatting dates without time, created when first 
needed.
+     *
+     * @see #getDateFormat(boolean)
+     */
+    private DateFormat dateFormat;
+
+    /**
+     * The object to use for formatting dates with time, created when first 
needed.
+     *
+     * @see #getDateFormat(boolean)
+     */
+    private DateFormat dateAndTimeFormat;
+
+    /**
+     * Creates a new widget.
+     *
+     * @param  locale  the locale to use for axis labels, or {@code null} for 
a default locale.
+     */
+    public GridSliceSelector(final Locale locale) {
+        this.locale = locale;
+        view = new GridPane();
+        view.setVgap(9);
+        view.setHgap(12);
+        view.setPadding(PADDING);
+        gridGeometry = new SimpleObjectProperty<>(this, "gridGeometry");
+        gridGeometry.addListener((p,o,n) -> setGridGeometry(n));
+    }
+
+    /**
+     * Invoked when the grid extent changed. This method adds or removes 
sliders as needed
+     * and configure the slider ranges for the new grid.
+     *
+     * @param  gg  the new grid geometry (potentially null).
+     */
+    private void setGridGeometry(final GridGeometry gg) {
+        final ObservableList<Node> children = view.getChildren();
+        if (gg == null || gg.getDimension() <= BIDIMENSIONAL || 
!gg.isDefined(GridGeometry.EXTENT)) {
+            children.clear();
+            return;
+        }
+        TransformSeparator gridToCRS  = null;       // Created when first 
needed.
+        Envelope           envelope   = null;       // Fetched when first 
needed.
+        double[]           resolution = null;       // Fetched when first 
needed.
+        Vocabulary         vocabulary = null;       // Fetched when first 
needed.
+
+        int childrenCount = 0;
+        int row = -BIDIMENSIONAL - 1;
+        final GridExtent extent = gg.getExtent();
+        final int dimension = extent.getDimension();
+        for (int dim=0; dim < dimension; dim++) {
+            final long min = extent.getLow (dim);
+            final long max = extent.getHigh(dim);
+            if (min < max && ++row >= 0) {
+                /*
+                 * 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.
+                 */
+                final Label     label;
+                final Slider    slider;
+                final Converter converter;
+                if (childrenCount < children.size()) {
+                    label     = (Label)  children.get(childrenCount++);
+                    slider    = (Slider) children.get(childrenCount++);
+                    converter = (Converter) slider.getLabelFormatter();
+                } else {
+                    childrenCount += 2;
+                    view.add(label  = new Label(),  0, row);
+                    view.add(slider = new Slider(), 1, row);
+                    slider.setShowTickLabels(true);
+                    slider.setShowTickMarks(true);
+                    slider.setBlockIncrement(1);
+                    label.setLabelFor(slider);
+                    GridPane.setHgrow(label,  Priority.NEVER);
+                    GridPane.setHgrow(slider, Priority.ALWAYS);
+                    GridPane.setValignment(label, VPos.TOP);
+                    converter = new Converter();
+                    slider.setLabelFormatter(converter);
+                    slider.widthProperty().addListener(converter);
+                }
+                /*
+                 * Configure the slider for the current grid axis.
+                 */
+                if (row == 0) {
+                    vocabulary = Vocabulary.getResources(locale);
+                    if (gg.isDefined(GridGeometry.GRID_TO_CRS)) {
+                        gridToCRS = new 
TransformSeparator(gg.getGridToCRS(PixelInCell.CELL_CENTER));
+                    }
+                    if (gg.isDefined(GridGeometry.ENVELOPE)) {
+                        envelope = gg.getEnvelope();
+                    }
+                    if (gg.isDefined(GridGeometry.RESOLUTION)) {
+                        resolution = gg.getResolution(false);
+                    }
+                }
+                slider.setMin(min);
+                slider.setMax(max);
+                slider.setValue(min);
+                converter.configure(gg, gridToCRS, dim, min, max, envelope, 
resolution);
+                converter.setTickSpacing(slider, slider.getWidth());
+                /*
+                 * Use the coordinate system axis abbreviation with its unit 
as a label,
+                 * or default to grid axis name if we have ne information 
about the CRS.
+                 */
+                final String name;
+                final DimensionNameType axis = 
extent.getAxisType(dim).orElse(null);
+                if (axis != null) {
+                    name = Types.getCodeTitle(axis).toString(locale);
+                } else {
+                    name = vocabulary.getString(Vocabulary.Keys.Axis_1, dim);
+                }
+                label.setText(vocabulary.toLabel(name));
+            }
+        }
+        children.remove(childrenCount, children.size());
+    }
+
+    /**
+     * Handle conversion of grid indices to "real world" coordinates or dates.
+     */
+    private final class Converter extends StringConverter<Double> implements 
ChangeListener<Number> {
+        /**
+         * Conversion from grid indices to "real world" coordinates, or {@code 
null} if none.
+         */
+        private MathTransform1D gridToCRS;
+
+        /**
+         * The coordinate system axis, or {@code null} if none.
+         */
+        private CoordinateSystemAxis axis;
+
+        /**
+         * If the axis is a temporal axis, a converter of axis values to dates.
+         * Otherwise {@code null}.
+         */
+        private DefaultTemporalCRS timeCRS;
+
+        /**
+         * The minimal temporal resolution value for allowing the formatter
+         * to omit the time field after the date field.
+         */
+        private double timeResolutionThreshold;
+
+        /**
+         * The resolution in CRS axis unit, or 1 if unknown.
+         */
+        private double resolution;
+
+        /**
+         * A conversion factor for computing the space between major tick, or 
0 or {@link Double#NaN} if unknown.
+         * This is the estimated maximal label length (in pixels) multiplied 
by the span in units of the CRS axis.
+         * This value shall be divided by the slider width (in pixels) for 
getting the space between major ticks
+         * in units of CRS axis.
+         */
+        private double spacingNumerator;
+
+        /**
+         * Creates a new converter.
+         */
+        Converter() {
+        }
+
+        /**
+         * Creates a new converter.
+         *
+         * @param gg   the grid geometry for which to create a converter. Can 
not be null.
+         * @param ts   a transform separator initialized to the coverage "grid 
to CRS" transform, or {@code null}.
+         * @param dim  the source dimension (grid axis) to extract in the 
{@code ts} separator.
+         * @param min  minimal grid coordinate (before conversion to CRS 
coordinate).
+         * @param max  maximal grid coordinate (before conversion to CRS 
coordinate).
+         * @param env  the envelope in CRS units, or {@code null} if unknown.
+         * @param res  the exact resolution (no estimation) for each CRS axis, 
or {@code null} if unknown.
+         */
+        final void configure(final GridGeometry gg, final TransformSeparator 
ts, final int dim,
+                    final double min, final double max, final Envelope env, 
final double[] res)
+        {
+            gridToCRS   = null;
+            axis        = null;
+            timeCRS     = null;
+            resolution  = 1;
+            timeResolutionThreshold = Double.NaN;
+            double span = max - min;
+            if (ts != null) try {
+                ts.clear();
+                ts.addSourceDimensions(dim);
+                gridToCRS = (MathTransform1D) ts.separate();
+                final int targetDim = ts.getTargetDimensions()[0];      // 
Must be after call to `separate(…)`.
+                if (gg.isDefined(GridGeometry.CRS)) {
+                    final CoordinateReferenceSystem crs = 
gg.getCoordinateReferenceSystem();
+                    final CoordinateReferenceSystem c = 
CRS.getComponentAt(crs, targetDim, targetDim+1);
+                    timeCRS = (c instanceof TemporalCRS) ? 
DefaultTemporalCRS.castOrCopy((TemporalCRS) c) : null;
+                    axis    = crs.getCoordinateSystem().getAxis(targetDim);
+                }
+                /*
+                 * `span` and `resolution` must be updated together, assuming 
linear conversion.
+                 * If the conversion is non-linear, then tick spacing 
computations will use grid
+                 * coordinates instead of "real world" coordinates.
+                 */
+                if (env != null && res != null && res[targetDim] > 0) {
+                    resolution = res[targetDim];
+                    span = env.getSpan(targetDim);
+                    timeResolutionThreshold = 
timeCRS.toValue(Duration.ofDays(1));
+                }
+            } catch (FactoryException | ClassCastException e) {
+                
Logging.ignorableException(Logger.getLogger(Modules.APPLICATION), 
GridSliceSelector.class,
+                        "setGridGeometry", e);              // 
"gridGeometry.set(…)" is the public API.
+            }
+            /*
+             * After the converter is fully configured (except for 
`spacingNumerator`),
+             * format two arbitrary values in order to estimate the size of 
labels.
+             * Note that the min/max values need to be grid indices, not 
envelope.
+             */
+            final int length = Math.max(toString(min).length(), 
toString(max).length());
+            spacingNumerator = length * PIXELS_PER_CHAR * span;
+        }
+
+        /**
+         * Sets the spacing between marks after the configuration changed or 
the slider width changed.
+         *
+         * @param  slider  the slider to configure.
+         * @param  width   the new slider width.
+         */
+        final void setTickSpacing(final Slider slider, final double width) {
+            double spacing = spacingNumerator / width;                         
     // In units of the CRS axis.
+            spacing = MathFunctions.pow10(Math.ceil(Math.log10(spacing)));     
     // Round to 0.1, 1, 10, 100…
+            spacing = Math.max(Math.rint(spacing / resolution), 1);            
     // Convert to grid units.
+            if (spacing > 0 && spacing < Double.POSITIVE_INFINITY) {
+                final double minor = Math.max(Math.rint(spacing / 
MAX_MINOR_TICK_COUNT), 1);
+                final long   count = Math.max(Math.min(Math.round(spacing / 
minor), MAX_MINOR_TICK_COUNT), 1) - 1;
+                slider.setMinorTickCount((int) count);
+                slider.setMajorTickUnit(spacing);
+                slider.setSnapToTicks(minor == 1);
+                if (timeCRS != null) {
+                    timeResolutionThreshold = 
timeCRS.toValue(Duration.ofDays(1)) / spacing;
+                }
+            }
+        }
+
+        /**
+         * Invoked when the slider changed its size.
+         * This method updates the number of ticks based on available space.
+         */
+        @Override
+        public void changed(final ObservableValue<? extends Number> property, 
final Number oldValue, final Number newValue) {
+            setTickSpacing((Slider) ((ReadOnlyProperty) property).getBean(), 
newValue.doubleValue());
+        }
+
+        /**
+         * Converts a grid index to a string representation.
+         */
+        @Override
+        public String toString(final Double index) {
+            double value = index;
+            double derivative;
+            int    numDigits;
+            if (gridToCRS != null) try {
+                value      = gridToCRS.transform(value);
+                derivative = gridToCRS.derivative(value);
+                numDigits  = 
DecimalFunctions.fractionDigitsForDelta(derivative, false);
+            } catch (TransformException e) {
+                return "N/A";
+            } else {
+                derivative = 0;
+                numDigits  = 0;
+            }
+            if (timeCRS != null) {
+                final DateFormat f = getDateFormat(derivative < 
timeResolutionThreshold);
+                return f.format(timeCRS.toDate(value));
+            } else {
+                final NumberFormat f = getNumberFormat();
+                f.setMinimumFractionDigits(numDigits);
+                f.setMaximumFractionDigits(numDigits);
+                return f.format(value);
+            }
+        }
+
+        /**
+         * Converts a string representation to a grid index.
+         * This method is defined as a matter of principle but should not be 
invoked.
+         */
+        @Override
+        public Double fromString(final String text) {
+            double value;
+            try {
+                if (timeCRS != null) {
+                    value = timeCRS.toValue(getDateFormat(true).parse(text));
+                } else {
+                    value = getNumberFormat().parse(text).doubleValue();
+                }
+                if (gridToCRS != null) {
+                    value = gridToCRS.inverse().transform(value);
+                }
+            } catch (ParseException | TransformException e) {
+                value = Double.NaN;
+            }
+            return value;
+        }
+    }
+
+    /**
+     * Returns the object to use for formatting numbers.
+     */
+    private NumberFormat getNumberFormat() {
+        if (numberFormat == null) {
+            numberFormat = (locale != null)
+                    ? NumberFormat.getNumberInstance(locale)
+                    : NumberFormat.getNumberInstance();
+        }
+        return numberFormat;
+    }
+
+    /**
+     * Returns the object to use for formatting dates.
+     *
+     * @param  withTime  {@code false} for dates only, or {@code true} for 
dates with times.
+     */
+    private DateFormat getDateFormat(final boolean withTime) {
+        if (withTime) {
+            if (dateAndTimeFormat == null) {
+                dateAndTimeFormat = (locale != null)
+                        ? DateFormat.getDateTimeInstance(DateFormat.SHORT, 
DateFormat.SHORT, locale)
+                        : DateFormat.getDateTimeInstance(DateFormat.SHORT, 
DateFormat.SHORT);
+            }
+            return dateAndTimeFormat;
+        } else {
+            if (dateFormat == null) {
+                dateFormat = (locale != null)
+                        ? DateFormat.getDateInstance(DateFormat.SHORT, locale)
+                        : DateFormat.getDateInstance(DateFormat.SHORT);
+            }
+            return dateFormat;
+        }
+    }
+
+    /**
+     * 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.
+     *
+     * @return the JavaFX component to insert in a scene graph.
+     */
+    @Override
+    public final Region getView() {
+        return view;
+    }
+
+    /**
+     * Returns {@code true} if this slice selector has no component to shown.
+     * Slice selectors are always empty with two-dimensional data.
+     *
+     * @return {@code true} if this slice selector has no component to shown.
+     */
+    public boolean isEmpty() {
+        return view.getChildren().isEmpty();
+    }
+}
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 0d30ed39f9..bbf6bebc18 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
@@ -17,6 +17,7 @@
 package org.apache.sis.gui.coverage;
 
 import javafx.geometry.Insets;
+import javafx.scene.Node;
 import javafx.scene.control.Control;
 import javafx.scene.control.Label;
 import javafx.scene.control.Toggle;
@@ -28,8 +29,10 @@ import javafx.scene.layout.Region;
 import javafx.scene.layout.VBox;
 import javafx.scene.text.Font;
 import javafx.scene.text.FontWeight;
+import javafx.collections.ObservableList;
 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.util.resources.IndexedResourceBundle;
@@ -64,6 +67,11 @@ abstract class ViewAndControls {
      */
     static final Insets CONTENT_MARGIN = new Insets(0, 0, 0, 
Styles.FORM_INSETS.getLeft());
 
+    /**
+     * Index of {@link #sliceSelector} in the list of children of {@link 
#viewAndNavigation}.
+     */
+    private static final int SLICE_SELECTOR_INDEX = 2;
+
     /**
      * The toolbar button for selecting this view.
      * This is initialized after construction and only if a button bar exists.
@@ -71,7 +79,7 @@ abstract class ViewAndControls {
     Toggle selector;
 
     /**
-     * The main component which is showing coverage data or image together 
with status bar.
+     * The main component which is showing coverage data or image together 
with status bar and {@link #sliceSelector}.
      * This is the component to show on the right (largest) part of the split 
pane.
      */
     final VBox viewAndNavigation;
@@ -92,18 +100,17 @@ abstract class ViewAndControls {
      */
     private Accordion controls;
 
+    /**
+     * The control for selecting a slice in a <var>n</var>-dimensional data 
cube.
+     */
+    protected final GridSliceSelector sliceSelector;
+
     /**
      * The widget which contain this view. This is the widget to inform when 
the coverage changed.
-     * Subclasses should define the following method:
      *
-     * {@preformat java
-     *     private void onPropertySet(final Resource resource, final 
GridCoverage data) {
-     *         // Update subclass-specific controls here, before to forward to 
explorer.
-     *         owner.notifyDataChanged(resource, data);
-     *     }
-     * }
+     * @see #notifyDataChanged(GridCoverageResource, GridCoverage)
      */
-    protected final CoverageExplorer owner;
+    private final CoverageExplorer owner;
 
     /**
      * Creates a new view-control pair.
@@ -112,6 +119,7 @@ abstract class ViewAndControls {
      */
     protected ViewAndControls(final CoverageExplorer owner) {
         this.owner = owner;
+        sliceSelector = new GridSliceSelector(owner.getLocale());
         viewAndNavigation = new VBox();
     }
 
@@ -121,9 +129,11 @@ abstract class ViewAndControls {
      */
     final void setView(final Region view, final StatusBar status) {
         final Region bar = status.getView();
+        final Region nav = sliceSelector.getView();
         VBox.setVgrow(view, Priority.ALWAYS);
         VBox.setVgrow(bar,  Priority.NEVER);
-        viewAndNavigation.getChildren().setAll(view, bar);
+        VBox.setVgrow(nav,  Priority.NEVER);
+        viewAndNavigation.getChildren().setAll(view, bar);      // `nav` will 
be added only when non-empty.
         SplitPane.setResizableWithParent(viewAndNavigation, Boolean.TRUE);
     }
 
@@ -150,6 +160,29 @@ 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.
+     *
+     * @param  resource  the new source of coverage, or {@code null} if none.
+     * @param  coverage  the new coverage, or {@code null} if none.
+     */
+    void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
+        sliceSelector.gridGeometry.set(coverage != null ? 
coverage.getGridGeometry() : null);
+        final ObservableList<Node> components = 
viewAndNavigation.getChildren();
+        final int count = components.size();
+        if (sliceSelector.isEmpty()) {
+            if (count > SLICE_SELECTOR_INDEX) {
+                components.remove(SLICE_SELECTOR_INDEX);
+            }
+        } else {
+            if (count <= SLICE_SELECTOR_INDEX) {
+                components.add(sliceSelector.getView());
+            }
+        }
+        owner.notifyDataChanged(resource, coverage);
+    }
+
 
 
 
diff --git 
a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/GridSliceSelectorApp.java
 
b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/GridSliceSelectorApp.java
new file mode 100644
index 0000000000..d667c3e9b0
--- /dev/null
+++ 
b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/GridSliceSelectorApp.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.coverage;
+
+import java.util.Locale;
+import javafx.stage.Stage;
+import javafx.scene.Scene;
+import javafx.scene.layout.Region;
+import javafx.application.Application;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.opengis.metadata.spatial.DimensionNameType;
+
+
+/**
+ * Shows selectors built by {@link GridSliceSelector} with arbitrary data.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public final strictfp class GridSliceSelectorApp extends Application {
+    /**
+     * Starts the test application.
+     *
+     * @param  args  ignored.
+     */
+    public static void main(final String[] args) {
+        launch(args);
+    }
+
+    /**
+     * Creates and starts the test application.
+     *
+     * @param  window  where to show the application.
+     */
+    @Override
+    public void start(final Stage window) {
+        final Scene scene = new Scene(createWidget());
+        window.setTitle("GridSliceSelector Test");
+        window.setScene(scene);
+        window.setWidth (400);
+        window.setHeight(300);
+        window.show();
+    }
+
+    /**
+     * Creates a view with arbitrary sliders to show.
+     */
+    private static Region createWidget() {
+        final DimensionNameType[] types = {
+            DimensionNameType.COLUMN,
+            DimensionNameType.ROW,
+            DimensionNameType.SAMPLE,
+            DimensionNameType.VERTICAL,
+            DimensionNameType.TIME
+        };
+        final GridExtent extent = new GridExtent(types,
+                new long[] {-100, -100, 20, 40, 1000},
+                new long[] { 500,  800, 20, 90, 1200}, true);
+        final GridSliceSelector selector = new 
GridSliceSelector(Locale.getDefault());
+        selector.gridGeometry.set(new GridGeometry(extent, null, null));
+        return selector.getView();
+    }
+}
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index 82c14ca150..56dec908d8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@ -141,7 +141,7 @@ import org.opengis.geometry.Geometry;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -1349,6 +1349,7 @@ public final class CRS extends Static {
      * @since 0.5
      */
     public static CoordinateReferenceSystem 
getComponentAt(CoordinateReferenceSystem crs, int lower, int upper) {
+        if (crs == null) return null;     // Skip bounds check.
         int dimension = ReferencingUtilities.getDimension(crs);
         ArgumentChecks.ensureValidIndexRange(dimension, lower, upper);
 check:  while (lower != 0 || upper != dimension) {
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
index d8fd54b670..b1f110f9b7 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing.crs;
 import java.util.Map;
 import java.util.Date;
 import java.time.Instant;
+import java.time.Duration;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import javax.xml.bind.annotation.XmlType;
@@ -65,7 +66,7 @@ import static 
org.apache.sis.internal.util.StandardDateFormat.MILLIS_PER_SECOND;
  * in the javadoc, this condition holds if all components were created using 
only SIS factories and static constants.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.3
  *
  * @see org.apache.sis.referencing.datum.DefaultTemporalDatum
  * @see org.apache.sis.referencing.cs.DefaultTimeCS
@@ -357,6 +358,27 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
         }
     }
 
+    /**
+     * Converts the given value difference into a duration object.
+     * If the given value {@linkplain Double#isNaN is NaN} or infinite,
+     * or if the conversion is non-linear, then this method returns {@code 
null}.
+     * This method is the converse of {@link #toValue(Duration)}.
+     *
+     * @param  delta  a difference of values in this axis. Unit of measurement 
is given by {@link #getUnit()}.
+     * @return the value difference as a duration, or {@code null} if the 
duration can not be computed.
+     *
+     * @since 1.3
+     */
+    public Duration toDuration(double delta) {
+        delta *= Units.derivative(toSeconds, Double.NaN);
+        if (Double.isFinite(delta)) {
+            final long t = Math.round(delta);
+            return Duration.ofSeconds(t, Math.round((delta - t) * 
NANOS_PER_SECOND));
+        } else {
+            return null;
+        }
+    }
+
     /**
      * Converts the given instant into a value in this axis unit.
      * If the given instant is {@code null}, then this method returns {@link 
Double#NaN}.
@@ -402,12 +424,36 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
         }
     }
 
+    /**
+     * Converts the given duration into a difference of values in this axis 
unit.
+     * If the given duration is {@code null}, or if the conversion is 
non-linear,
+     * then this method returns {@link Double#NaN}.
+     * This method is the converse of {@link #toDuration(double)}.
+     *
+     * @param  delta  the difference of values as a duration, or {@code null}.
+     * @return the value difference in this axis unit, or {@link Double#NaN} 
if it can not be computed.
+     *         Unit of measurement is given by {@link #getUnit()}.
+     *
+     * @since 1.3
+     */
+    public double toValue(final Duration delta) {
+        if (delta != null) {
+            double t = delta.getSeconds();
+            t += delta.getNano() / (double) NANOS_PER_SECOND;
+            t *= Units.derivative(toSeconds.inverse(), Double.NaN);
+            return t;
+        } else {
+            return Double.NaN;
+        }
+    }
+
     /**
      * Formats this CRS as a <cite>Well Known Text</cite> {@code TimeCRS[…]} 
element.
      *
      * <div class="note"><b>Compatibility note:</b>
      * {@code TimeCRS} is defined in the WKT 2 specification only.</div>
      *
+     * @param  formatter  the formatter where to format the inner content of 
this WKT element.
      * @return {@code "TimeCRS"}.
      *
      * @see <a 
href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#88";>WKT 2 
specification §14</a>
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/package-info.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/package-info.java
index 8c7a3103cf..f3e34038a4 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/package-info.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/package-info.java
@@ -73,7 +73,7 @@
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Cédric Briançon (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.4
  * @module
  */
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java
index b347b7e9a3..9559caf219 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultTemporalCRSTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.referencing.crs;
 
+import java.time.Duration;
 import java.time.Instant;
 import java.util.Date;
 import java.util.Collections;
@@ -34,7 +35,7 @@ import static 
org.apache.sis.internal.util.StandardDateFormat.NANOS_PER_MILLISEC
  * Tests {@link DefaultTemporalCRS}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.5
  * @module
  */
@@ -126,4 +127,16 @@ public final strictfp class DefaultTemporalCRSTest extends 
TestCase {
         assertEquals("toInstant", t,            crs.toInstant(v));
         assertEquals("toDate",    Date.from(t), crs.toDate(v));
     }
+
+    /**
+     * Tests {@link DefaultTemporalCRS#toDuration(double)} and its converse.
+     */
+    @Test
+    public void testDurationConversion() {
+        final DefaultTemporalCRS crs = HardCodedCRS.TIME;
+        final Duration duration = crs.toDuration(4.25);
+        assertEquals(  4, duration.toDays());
+        assertEquals(102, duration.toHours());
+        assertEquals(4.25, crs.toValue(duration), STRICT);
+    }
 }
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
index a85e44d24a..db684090e6 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
@@ -84,6 +84,9 @@ abstract class AbstractConverter implements UnitConverter, 
Serializable {
 
     /**
      * Returns the derivative of the conversion function at the given value, 
or {@code NaN} if unknown.
+     *
+     * @param  value  the point at which to compute the derivative.
+     *                Ignored (can be {@link Double#NaN}) if the conversion is 
linear.
      */
     public abstract double derivative(double value);
 
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
index b7cde1c8e1..0cad26f04f 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
@@ -1684,6 +1684,7 @@ public final class Units extends Static {
      *
      * @param  converter  the converter for which we want the derivative at a 
given point, or {@code null}.
      * @param  value      the point at which to compute the derivative.
+     *                    Ignored (can be {@link Double#NaN}) if the 
conversion is linear.
      * @return the derivative at the given point, or {@code NaN} if unknown.
      *
      * @see 
org.apache.sis.referencing.operation.transform.AbstractMathTransform#derivative(DirectPosition)
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
index 0a593eed82..1a3aef3a04 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
@@ -77,7 +77,7 @@ import static java.util.logging.Logger.getLogger;
  * multiple threads.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -494,6 +494,16 @@ public class IndexedResourceBundle extends ResourceBundle 
implements Localized {
         }
     }
 
+    /**
+     * Returns the given string followed by a colon.
+     *
+     * @param  text  the text to follow be a colon.
+     * @return the given text followed by a colon.
+     */
+    public final String toLabel(final String text) {
+        return text.concat(colon());
+    }
+
     /**
      * Returns the localized string identified by the given key followed by a 
colon.
      * This is the same functionality as {@link #appendLabel(short, 
Appendable)} but
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index 48f224a8ff..ae677139d3 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -114,6 +114,11 @@ public final class Vocabulary extends 
IndexedResourceBundle {
          */
         public static final short AxisChanges = 12;
 
+        /**
+         * Axis {0}
+         */
+        public static final short Axis_1 = 269;
+
         /**
          * Azimuth
          */
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index b4fe75867d..e91653a67a 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -25,6 +25,7 @@ AngularMinutes          = Minutes
 AngularSeconds          = Seconds
 Attributes              = Attributes
 Automatic               = Automatic
+Axis_1                  = Axis {0}
 AxisChanges             = Axis changes
 Azimuth                 = Azimuth
 Background              = Background
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 940a4dfebc..b1c43ab222 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -32,6 +32,7 @@ AngularMinutes          = Minutes
 AngularSeconds          = Secondes
 Attributes              = Attributs
 Automatic               = Automatique
+Axis_1                  = Axe {0}
 AxisChanges             = Changements d\u2019axes
 Azimuth                 = Azimut
 Background              = Arri\u00e8re plan
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
index f6be3048f3..8e9d7e4bfc 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
@@ -82,7 +82,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see java.util.ResourceBundle
  * @see java.text.MessageFormat

Reply via email to