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