sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 03/03: Show coverage sample dimensions in a table, with min/max values and units of measurement.
Date Sat, 01 Feb 2020 14:07:51 GMT
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 <martin.desruisseaux@geomatys.com>
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


Mime
View raw message