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 8c19cc083b813cdaa99451ec60158f5ff070030b Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Feb 1 15:07:16 2020 +0100 Show coverage sample dimensions in a table, with min/max values and units of measurement. --- .../sis/gui/coverage/CategoryCellFactory.java | 157 +++++++++++++++++++++ .../org/apache/sis/gui/coverage/CellFormat.java | 18 ++- .../apache/sis/gui/coverage/CoverageExplorer.java | 48 ++++--- .../java/org/apache/sis/gui/coverage/GridView.java | 2 +- .../sis/util/resources/IndexedResourceBundle.java | 32 ++++- .../org/apache/sis/util/resources/Vocabulary.java | 10 ++ .../sis/util/resources/Vocabulary.properties | 2 + .../sis/util/resources/Vocabulary_fr.properties | 2 + 8 files changed, 241 insertions(+), 30 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryCellFactory.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryCellFactory.java new file mode 100644 index 0000000..412495a --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryCellFactory.java @@ -0,0 +1,157 @@ +/* + * 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.Optional; +import javafx.util.Callback; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableView; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableColumn.CellDataFeatures; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Pos; +import org.apache.sis.measure.NumberRange; +import org.apache.sis.coverage.Category; +import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.util.resources.Vocabulary; + + +/** + * Factory methods for cell values to shown in the sample dimension or category table. + * + * <p>Current implementation handles only {@link SampleDimension} instances, but it + * can be extended for handling also {@link Category} objects in a future version. + * The class name is in anticipation for such extension.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.1 + * @since 1.1 + * @module + */ +final class CategoryCellFactory implements Callback<TableColumn<SampleDimension,Number>, TableCell<SampleDimension,Number>> { + /** + * Identifier of columns shown in the sample dimension or category table. + */ + private static final String NAME = "name", MINIMUM = "minimum", MAXIMUM = "maximum", UNITS = "units"; + + /** + * The format to use for formatting minimum and maximum values. + */ + private final CellFormat cellFormat; + + /** + * Creates a cell value factory. + */ + CategoryCellFactory(final CellFormat format) { + cellFormat = format; + } + + /** + * Creates a table of sample dimensions. + * + * @param vocabulary resources for the locale in use. + */ + TableView<SampleDimension> createSampleDimensionTable(final Vocabulary vocabulary) { + final TableView<SampleDimension> table = new TableView<>(); + table.setPrefHeight(5 * 30); // Show approximately 5 rows. + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + table.getColumns().setAll( + createStringColumn(vocabulary, Vocabulary.Keys.Name, NAME), + createNumberColumn(vocabulary, Vocabulary.Keys.Minimum, MINIMUM), + createNumberColumn(vocabulary, Vocabulary.Keys.Maximum, MAXIMUM), + createStringColumn(vocabulary, Vocabulary.Keys.Units, UNITS)); + return table; + } + + /** + * Creates a new column with a title identified by the given key. + */ + private TableColumn<SampleDimension,String> createStringColumn(final Vocabulary vocabulary, final short key, final String id) { + final TableColumn<SampleDimension,String> column = new TableColumn<>(vocabulary.getString(key)); + column.setCellValueFactory(CategoryCellFactory::getStringValue); + column.setId(id); + return column; + } + + /** + * Creates a new column with a title identified by the given key. + */ + private TableColumn<SampleDimension,Number> createNumberColumn(final Vocabulary vocabulary, final short key, final String id) { + final TableColumn<SampleDimension,Number> column = new TableColumn<>(vocabulary.getString(key)); + column.setCellValueFactory(this::getNumberValue); + column.setCellFactory(this); + column.setId(id); + return column; + } + + /** + * Creates a cell for numeric values. + * This method is public as an implementation side-effect; do not rely on that. + */ + @Override + public TableCell<SampleDimension,Number> call(final TableColumn<SampleDimension,Number> column) { + return new Numeric(cellFormat); + } + + /** + * Cell for numeric values. + */ + private static final class Numeric extends TableCell<SampleDimension,Number> { + /** The format to use for formatting minimum and maximum values. */ + private final CellFormat cellFormat; + + /** Creates a new numeric cell. */ + Numeric(final CellFormat format) { + cellFormat = format; + setAlignment(Pos.CENTER_RIGHT); + } + + /** Invoked when a new numeric value is set in this cell. */ + @Override public void updateItem(final Number item, final boolean empty) { + setText(empty || item == null ? "" : cellFormat.format(item)); + } + } + + /** + * Invoked when the table needs to render a textual cell in the sample dimension table. + */ + private static ObservableValue<String> getStringValue(final CellDataFeatures<SampleDimension,String> cell) { + final Optional<?> text; + final SampleDimension sd = cell.getValue(); + switch (cell.getTableColumn().getId()) { + case NAME: text = Optional.ofNullable(sd.getName()); break; + case UNITS: text = sd.getUnits(); break; + default: throw new AssertionError(); // Should not happen. + } + return text.map(Object::toString).map(ReadOnlyObjectWrapper::new).orElse(null); + } + + /** + * Invoked when the table need to render a numeric cell in the sample dimension table. + */ + private ObservableValue<Number> getNumberValue(final CellDataFeatures<SampleDimension,Number> cell) { + final Optional<Number> value; + final Optional<NumberRange<?>> range = cell.getValue().getSampleRange(); + switch (cell.getTableColumn().getId()) { + case MINIMUM: value = range.map(NumberRange::getMinValue); break; + case MAXIMUM: value = range.map(NumberRange::getMaxValue); break; + default: throw new AssertionError(); // Should not happen. + } + return value.map(ReadOnlyObjectWrapper::new).orElse(null); + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java index 0b0eeeb..911cf29 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java @@ -133,7 +133,7 @@ final class CellFormat { cellFormat.setMaximumFractionDigits(n); } buffer.setLength(0); - format(lastValue); + formatCell(lastValue); } /** @@ -154,7 +154,7 @@ final class CellFormat { } else { final double value = tile.getSampleDouble(x, y, b); if (Double.doubleToRawLongBits(value) != Double.doubleToRawLongBits(lastValue)) { - format(value); + formatCell(value); lastValue = value; } } @@ -163,8 +163,12 @@ final class CellFormat { /** * Formats the given sample value and stores the result in {@link #lastValueAsText}. + * This method should be invoked only for real numbers, not when the number is known + * to be an integer value. This method is designed for cell values in {@link GridView}, + * where {@link Double#NaN} values are represented by a more compact symbol. + * For formatting in other contexts, use {@link #format(Number)} instead. */ - private void format(final double value) { + private void formatCell(final double value) { if (Double.isNaN(value)) { lastValueAsText = "⬚"; } else { @@ -173,6 +177,14 @@ final class CellFormat { } /** + * Formats the given sample value. + */ + final String format(final Number value) { + buffer.setLength(0); + return cellFormat.format(value, buffer, formatField).toString(); + } + + /** * Formats the given value using the given formatter. This is used for indices * in row header or column header, which have their own formatter. */ 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 d45a69a..06d6dbc 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 @@ -22,12 +22,12 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; +import javafx.geometry.Insets; import javafx.scene.control.Accordion; import javafx.scene.control.Label; -import javafx.scene.control.ListView; import javafx.scene.control.SplitPane; +import javafx.scene.control.TableView; import javafx.scene.control.TitledPane; -import javafx.scene.control.ListCell; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import org.apache.sis.coverage.SampleDimension; @@ -47,6 +47,11 @@ import org.apache.sis.util.resources.Vocabulary; */ public class CoverageExplorer { /** + * Margin to keep around captions on top of tables or lists. + */ + private static final Insets CAPTION_MARGIN = new Insets(12, 0, 9, 0); + + /** * The data shown in this table. Note that setting this property to a non-null value may not * modify the grid content immediately. Instead, a background process will request the tiles. * @@ -66,7 +71,7 @@ public class CoverageExplorer { /** * The control showing sample dimensions for the current coverage. */ - private final ListView<SampleDimension> sampleDimensions; + private final TableView<SampleDimension> sampleDimensions; /** * The control that put everything together. @@ -80,26 +85,29 @@ public class CoverageExplorer { * Creates an initially empty explorer. */ public CoverageExplorer() { - gridView = new GridView(); - sampleDimensions = new ListView<>(); - sampleDimensions.setCellFactory(SampleDimensionCell::new); final Vocabulary vocabulary = Vocabulary.getResources((Locale) null); + gridView = new GridView(); + sampleDimensions = new CategoryCellFactory(gridView.cellFormat).createSampleDimensionTable(vocabulary); /* - * Create the "Coverage" pane with the following controls: + * "Coverage" section with the following controls: * - Coverage domain as a list of CRS dimensions with two of them selected (TODO). * - Coverage range as a list of sample dimensions with at least one selected. */ final VBox coveragePane; { // Block for making variables locale to this scope. - final Label label = new Label(vocabulary.getString(Vocabulary.Keys.SampleDimensions)); + final Label label = new Label(vocabulary.getLabel(Vocabulary.Keys.SampleDimensions)); + label.setPadding(CAPTION_MARGIN); label.setLabelFor(sampleDimensions); coveragePane = new VBox(label, sampleDimensions); } - + /* + * Put all sections together and have the first one expanded by default. + */ final Accordion controls = new Accordion( new TitledPane(vocabulary.getString(Vocabulary.Keys.Coverage), coveragePane) // TODO: more controls to be added in a future version. ); + controls.setExpandedPane(controls.getPanes().get(0)); content = new SplitPane(controls, gridView); SplitPane.setResizableWithParent(controls, Boolean.FALSE); SplitPane.setResizableWithParent(gridView, Boolean.TRUE); @@ -176,10 +184,8 @@ public class CoverageExplorer { gridView.setImage((RenderedImage) null); if (coverage != null) { gridView.setImage(new ImageRequest(coverage, null)); // Start a background thread. - sampleDimensions.getItems().setAll(coverage.getSampleDimensions()); - } else { - sampleDimensions.getItems().clear(); } + onLoadStep(coverage); } /** @@ -194,23 +200,19 @@ public class CoverageExplorer { final ObservableList<SampleDimension> items = sampleDimensions.getItems(); if (coverage != null) { items.setAll(coverage.getSampleDimensions()); + sampleDimensions.getSelectionModel().clearAndSelect(gridView.getBand()); } else { items.clear(); } } /** - * A row in the list of sample dimensions. + * Invoked when the selected band changed. This method ensures that the selected row + * in the sample dimension table matches the band which is shown in the grid view. */ - private static final class SampleDimensionCell extends ListCell<SampleDimension> { - /** Invoked by lambda function (needs this exact signature). */ - SampleDimensionCell(final ListView<SampleDimension> list) { - } - - /** Invoked when a new sample dimension needs to be shown. */ - @Override public void updateItem(final SampleDimension item, final boolean empty) { - super.updateItem(item, empty); - setText(empty ? "" : item.getName().toString()); - } + private void onBandSpecified(final ObservableValue<? extends Number> property, + final Number previous, final Number band) + { + sampleDimensions.getSelectionModel().clearAndSelect(band.intValue()); } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java index a6a091e..09734fe 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java @@ -184,7 +184,7 @@ public class GridView extends Control { /** * The formatter to use for writing sample values. */ - private final CellFormat cellFormat; + final CellFormat cellFormat; /** * Creates an initially empty grid view. The content can be set after 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 7904b91..4f36676 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 @@ -75,7 +75,7 @@ import org.apache.sis.measure.Range; * multiple threads. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.0 + * @version 1.1 * @since 0.3 * @module */ @@ -484,14 +484,40 @@ public class IndexedResourceBundle extends ResourceBundle implements Localized { */ public final void appendLabel(final short key, final Appendable toAppendTo) throws IOException { toAppendTo.append(getString(key)); - if (Locale.FRENCH.getLanguage().equals(getLocale().getLanguage())) { - toAppendTo.append("\u00A0:"); + final String colon = colon(); + if (colon != null) { + toAppendTo.append(colon); } else { toAppendTo.append(':'); } } /** + * Returns the localized string identified by the given key followed by a colon. + * This is the same functionality as {@link #appendLabel(short, Appendable)} but + * without temporary buffer. + * + * @param key the key for the desired string. + * @return localized string followed by a colon. + * + * @since 1.1 + */ + public final String getLabel(final short key) { + final String text = getString(key); + final String colon = colon(); + return (colon != null) ? (text + colon) : (text + ':'); + } + + /** + * Returns the colon to write after localized text. + * + * @todo Should be a localized resource by itself. + */ + private String colon() { + return Locale.FRENCH.getLanguage().equals(getLocale().getLanguage()) ? "\u00A0:" : null; + } + + /** * Gets a string for the given key from this resource bundle or one of its parents. * * @param key the key for the desired string. 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 9f3d6c0..43d9ed1 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 @@ -585,6 +585,11 @@ public final class Vocabulary extends IndexedResourceBundle { public static final short Mapping = 96; /** + * Maximum + */ + public static final short Maximum = 191; + + /** * Maximum value */ public static final short MaximumValue = 97; @@ -605,6 +610,11 @@ public final class Vocabulary extends IndexedResourceBundle { public static final short Methods = 100; /** + * Minimum + */ + public static final short Minimum = 192; + + /** * Minimum value */ public static final short MinimumValue = 101; 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 f605ae9..bf0cfdc 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 @@ -120,8 +120,10 @@ LowerBound = Lower bound Magenta = Magenta Mandatory = Mandatory Mapping = Mapping +Maximum = Maximum MaximumValue = Maximum value MeanValue = Mean value +Minimum = Minimum MinimumValue = Minimum value MissingValue = Missing value Measures = Measures 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 f10f4f1..bd0ee7b 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 @@ -127,8 +127,10 @@ LowerBound = Limite basse Magenta = Magenta Mandatory = Requis Mapping = Cartographie +Maximum = Maximum MaximumValue = Valeur maximale MeanValue = Valeur moyenne +Minimum = Minimum MinimumValue = Valeur minimale MissingValue = Valeur manquante Measures = Mesures
