sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/02: First revision of FeatureTable. Data are loaded in a background thread by pages of 100 features. The work is not complete: the loading of next page is not yet delayed until first needed.
Date Wed, 06 Nov 2019 00:05:45 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 41e610d77731dbc7161af4cf3ddd72bcf18bd14e
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Nov 6 00:57:26 2019 +0100

    First revision of FeatureTable. Data are loaded in a background thread by pages of 100
features.
    The work is not complete: the loading of next page is not yet delayed until first needed.
---
 .../org/apache/sis/gui/dataset/FeatureTable.java   | 557 +++++++++++++++++----
 .../apache/sis/gui/dataset/ResourceExplorer.java   |  29 +-
 .../apache/sis/gui/metadata/MetadataSummary.java   |  16 +-
 .../org/apache/sis/gui/metadata/MetadataTree.java  |  24 +-
 .../java/org/apache/sis/gui/metadata/Section.java  |   4 +-
 .../org/apache/sis/internal/gui/Resources.java     |  10 +
 .../apache/sis/internal/gui/Resources.properties   |   2 +
 .../sis/internal/gui/Resources_fr.properties       |   2 +
 .../sis/util/collection/BackingStoreException.java |  22 +-
 9 files changed, 535 insertions(+), 131 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
index a151e25..10a9da3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
@@ -16,154 +16,495 @@
  */
 package org.apache.sis.gui.dataset;
 
-import java.util.Map;
-import java.util.HashMap;
+import java.util.Locale;
 import java.util.List;
-import java.util.Iterator;
-import java.util.ResourceBundle;
-import java.util.MissingResourceException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Spliterator;
 import java.util.stream.Stream;
-import java.util.stream.Collectors;
-import javafx.collections.FXCollections;
-import javafx.scene.control.Label;
-import javafx.scene.control.ScrollPane;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.CancellationException;
+import javafx.application.Platform;
 import javafx.scene.control.TableColumn;
 import javafx.scene.control.TableView;
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-import javafx.scene.layout.BorderPane;
-import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.concurrent.Task;
+import javafx.util.Callback;
 import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
-import org.opengis.geometry.Geometry;
-import org.apache.sis.internal.util.CheckedArrayList;
-import org.apache.sis.storage.DataStoreException;
+import org.opengis.util.InternationalString;
+import org.apache.sis.internal.util.Strings;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.Resource;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.storage.DataStoreException;
 
 
 /**
+ * A view of {@link FeatureSet} data organized as a table. The features are specified by
a call
+ * to {@link #setFeatures(FeatureSet)}, which will load the features in a background thread.
+ * At first only {@value #PAGE_SIZE} features are loaded.
+ * More features will be loaded only when the user scroll down.
+ *
+ * <p>If this view is removed from scene graph, then {@link #interrupt()} should be
called
+ * for stopping any loading process that may be under progress.</p>
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Smaniotto Enzo (GSoC)
- * @version 1.0
- * @since   1.0
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
  * @module
  */
-public class FeatureTable extends BorderPane {
+public class FeatureTable extends TableView<Feature> {
+    /**
+     * Maximum number of features to load in a background task.
+     * If there is more features to load, we will use many tasks.
+     *
+     * @see #nextPageLoader
+     */
+    private static final int PAGE_SIZE = 100;
+
+    /**
+     * The locale to use for texts.
+     */
+    private final Locale textLocale;
+
+    /**
+     * The locale to use for dates/numbers.
+     * This is often the same than {@link #textLocale}.
+     */
+    private final Locale dataLocale;
+
+    /**
+     * The type of features, or {@code null} if not yet determined.
+     * This type determines the columns that will be shown.
+     *
+     * @see #setFeatureType(FeatureType)
+     */
+    private FeatureType featureType;
+
+    /**
+     * If not all features have been read, the task for loading the next batch of {@value
#PAGE_SIZE} features.
+     * This task will be executed only if there is a need to see new features.
+     *
+     * <p>If a loading is in progress, then this field is the loader doing the work.
+     * But this field will be updated with next loader as soon as the loading is completed.</p>
+     */
+    private Loader nextPageLoader;
+
+    /**
+     * Creates an initially empty table.
+     */
+    public FeatureTable() {
+        textLocale = Locale.getDefault(Locale.Category.DISPLAY);
+        dataLocale = Locale.getDefault(Locale.Category.FORMAT);
+        setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
+        setTableMenuButtonVisible(true);
+    }
+
+    /**
+     * Sets the features to show in this table. This method loads an arbitrary amount of
+     * features in a background thread. It does not load all features if the feature set
+     * is large, unless the user scroll down.
+     *
+     * <p>If the loading of another {@code FeatureSet} was in progress at the time
this method is invoked,
+     * then that previous loading process is cancelled.</p>
+     *
+     * <p><b>Note:</b> the table content may appear unmodified after this
method returns.
+     * The modifications will appear at an undetermined amount of time later.</p>
+     *
+     * @param  features  the features, or {@code null} if none.
+     */
+    public void setFeatures(final FeatureSet features) {
+        assert Platform.isFxApplicationThread();
+        final Loader previous = nextPageLoader;
+        if (previous != null) {
+            nextPageLoader = null;
+            previous.cancel();
+        }
+        if (features != null) {
+            setLoader(new InitialLoader(features));
+            BackgroundThreads.execute(nextPageLoader);
+        } else {
+            featureType = null;
+            getItems().clear();
+            getColumns().clear();
+        }
+    }
+
+    /**
+     * Sets {@link #nextPageLoader} to the given values and sets the listeners, but without
starting the task yet.
+     *
+     * @param  loader  the loader for next {@value #PAGE_SIZE} features,
+     *                 or {@code null} if there is no more features to load.
+     */
+    private void setLoader(final Loader loader) {
+        if (loader != null) {
+            loader.setOnSucceeded(this::addFeatures);
+            loader.setOnCancelled(this::cancelled);
+            loader.setOnFailed(this::cancelled);
+        }
+        nextPageLoader = loader;
+    }
+
     /**
-     * Contains ResourceBundles indexed by table names.
+     * Invoked in JavaFX thread after new feature instances are ready.
      */
-    private final Map<String, ResourceBundle> bundles = new HashMap<>();
+    private void addFeatures(final WorkerStateEvent event) {
+        assert Platform.isFxApplicationThread();
+        final Loader loader = (Loader) event.getSource();
+        if (loader == nextPageLoader) {
+            getItems().addAll((List<Feature>) event.getSource().getValue());
+            setLoader(nextPageLoader.next());
 
-    private String bundlePrefix;
+            // TODO: temporary hack: we should not start the job now, but wait until we need
it.
+            if (nextPageLoader != null) {
+                BackgroundThreads.execute(nextPageLoader);
+            }
+        } else try {
+            loader.close();
+        } catch (DataStoreException e) {
+            unexpectedException("addFeatures", e);
+        }
+    }
 
-    private String generateFinalColumnName(final PropertyType prop) {
-        Map<String, Map.Entry<String, String>> labelInfo = (Map) prop.getDesignation();
-        final String labelName = prop.getName().toString();
-        String columnName = labelName;
-        String tableName = null;
+    /**
+     * Invoked in JavaFX thread when a loading process has been cancelled or failed.
+     *
+     * @see #interrupt()
+     */
+    private void cancelled(final WorkerStateEvent event) {
+        assert Platform.isFxApplicationThread();
+        final Loader loader = (Loader) event.getSource();
+        final boolean isCurrentLoader = (loader == nextPageLoader);
+        if (isCurrentLoader) {
+            nextPageLoader = null;
+        }
         /*
-         * If exists, explore labelInfo to retrive table and column respect to this label.
+         * Loader should be already closed if error or cancellation happened during the reading
process.
+         * But it may not be closed if the task was cancelled before it started, or maybe
because of some
+         * other holes we missed. So close again as a double-check.
          */
-        if (labelInfo != null) {
-            final Map.Entry<String, String> entry = labelInfo.get(labelName);
-            if (entry != null) {
-                if (entry.getKey() != null) {
-                    tableName = entry.getKey();
-                } else {
-                    tableName = null;
+        Throwable exception = loader.getException();
+        try {
+            loader.close();
+        } catch (DataStoreException e) {
+            if (exception == null) {
+                exception = e;
+            } else {
+                exception.addSuppressed(e);
+            }
+        }
+        if (exception != null) {
+            if (isCurrentLoader) {
+                exception.printStackTrace();        // TODO: write somewhere in the widget.
+            } else {
+                // Since we moved to other data, not appropriate anymore for current widget.
+                unexpectedException("cancelled", exception);
+            }
+        }
+    }
+
+    /**
+     * A task to execute in background thread for fetching feature instances.
+     * This task does not load all features; only {@value #PAGE_SIZE} of them are loaded.
+     *
+     * <p>Loading processes are started by {@link InitialLoader}.
+     * Only additional pages are loaded by ordinary {@code Loader}.</p>
+     */
+    private static class Loader extends Task<List<Feature>> {
+        /**
+         * The stream to close after we finished to iterate over features.
+         * This stream should not be used for any other purpose.
+         */
+        private Stream<Feature> toClose;
+
+        /**
+         * If the reading process is not finished, the iterator for reading more feature
instances.
+         */
+        private Spliterator<Feature> iterator;
+
+        /**
+         * An estimation of the number of features, or {@link Long#MAX_VALUE} if unknown.
+         */
+        private long estimatedCount;
+
+        /**
+         * Creates a new loader. This constructor is for {@link InitialLoader} usage only.
+         */
+        Loader() {
+            estimatedCount = Long.MAX_VALUE;
+        }
+
+        /**
+         * Creates a new task for continuing the work of a previous task.
+         * The new task will load the next {@value #PAGE_SIZE} features.
+         */
+        private Loader(final Loader previous) {
+            toClose        = previous.toClose;
+            iterator       = previous.iterator;
+            estimatedCount = previous.estimatedCount;
+        }
+
+        /**
+         * Initializes this task for reading features from the specified set.
+         * This method shall be invoked by {@link InitialLoader} only.
+         */
+        final void initialize(final FeatureSet features) throws DataStoreException {
+            toClose        = features.features(false);
+            iterator       = toClose .spliterator();
+            estimatedCount = iterator.estimateSize();
+        }
+
+        /**
+         * If there is more features to load, returns a new task for loading the next
+         * {@value #PAGE_SIZE} features. Otherwise returns {@code null}.
+         */
+        final Loader next() {
+            return (iterator != null) ? new Loader(this) : null;
+        }
+
+        /**
+         * Loads up to {@value #PAGE_SIZE} features.
+         */
+        @Override
+        protected List<Feature> call() throws DataStoreException {
+            final Spliterator<Feature> it = iterator;
+            iterator = null;                                // Clear now in case an exception
happens below.
+            final List<Feature> instances = new ArrayList<>((int) Math.min(estimatedCount,
PAGE_SIZE));
+            if (it != null) try {
+                while (it.tryAdvance(instances::add)) {
+                    if (instances.size() >= PAGE_SIZE) {
+                        iterator = it;                      // Remember that there is more
instances to read.
+                        return instances;                   // Intentionally skip the call
to close().
+                    }
+                    if (isCancelled()) {
+                        break;
+                    }
+                }
+            } catch (BackingStoreException e) {
+                try {
+                    close();
+                } catch (DataStoreException s) {
+                    e.addSuppressed(s);
                 }
-                if (entry.getValue() != null) {
-                    columnName = entry.getValue();
+                throw e.unwrapOrRethrow(DataStoreException.class);
+            }
+            close();                                        // Loading has been cancelled.
+            return instances;
+        }
+
+        /**
+         * Closes the feature stream. This method can be invoked only when {@link #call()}
finished its work.
+         * It is safe to invoke this method again even if this loader has already been closed.
+         */
+        final void close() throws DataStoreException {
+            iterator = null;
+            final Stream<Feature> c = toClose;
+            if (c != null) try {
+                toClose = null;                             // Clear now in case an exception
happens below.
+                c.close();
+            } catch (BackingStoreException e) {
+                throw e.unwrapOrRethrow(DataStoreException.class);
+            }
+        }
+
+        /**
+         * Wait for {@link #call()} to finish its work either successfully or as a result
of cancellation,
+         * then close the stream. This method should be invoked in a background thread when
we don't know
+         * if the task is still running or not.
+         *
+         * @see FeatureTable#interrupt()
+         */
+        final void waitAndClose() {
+            Throwable error = null;
+            try {
+                get();      // Wait for the task to stop before to close the stream.
+            } catch (InterruptedException | CancellationException e) {
+                // Ignore, we will try to close the stream right now.
+                recoverableException("interrupt", e);
+            } catch (ExecutionException e) {
+                error = e.getCause();
+            }
+            try {
+                close();
+            } catch (DataStoreException e) {
+                if (error != null) {
+                    error.addSuppressed(e);
                 } else {
-                    columnName = labelName;
+                    error = e;
                 }
             }
+            if (error != null) {
+                // FeatureTable.interrupt is the public API calling this method.
+                unexpectedException("interrupt", error);
+            }
         }
-        /*
-         * If table name is not null, try to found resourcebundle for this table.
+    }
+
+    /**
+     * The task to execute in background thread for initiating the loading process.
+     * This tasks is created only for the first {@value #PAGE_SIZE} features.
+     * For all additional features, an ordinary {@link Loader} will be used.
+     */
+    private final class InitialLoader extends Loader {
+        /**
+         * The set of features to read.
+         */
+        private final FeatureSet features;
+
+        /**
+         * Initializes a new task for loading features from the given set.
+         */
+        InitialLoader(final FeatureSet features) {
+            this.features = features;
+        }
+
+        /**
+         * Gets the feature type, initialize the iterator and gets the first {@value #PAGE_SIZE}
features.
+         * The {@link FeatureType} should be given by {@link FeatureSet#getType()}, but this
method is
+         * robust to incomplete implementations where {@code getType()} returns {@code null}.
          */
-        if (tableName != null) {
+        @Override
+        protected List<Feature> call() throws DataStoreException {
+            final boolean isTypeKnown = setType(features.getType());
+            initialize(features);
+            final List<Feature> instances = super.call();
+            if (isTypeKnown) {
+                return instances;
+            }
             /*
-             * If there isn't resource bundles (or not for the curruen table), try to generate.
+             * Following code is a safety for FeatureSet that do not implement the `getType()`
method.
+             * This method is mandatory and implementation should not be allowed to return
null, but
+             * incomplete implementations exist so we are better to be safe. If we can not
get the type
+             * from the first feature instances, we will give up.
              */
-            if (bundles.get(tableName) == null) {
-                if (bundlePrefix != null) {
-                    bundles.put(tableName, ResourceBundle.getBundle(bundlePrefix + tableName));
+            for (final Feature f : instances) {
+                if (f != null && setType(f.getType())) {
+                    return instances;
                 }
             }
+            throw new DataStoreException(Resources.forLocale(textLocale).getString(Resources.Keys.NoFeatureTypeInfo));
         }
-        final ResourceBundle bundle = bundles.get(tableName);
-        String finalColumnName;
-        if (labelName == null) {
-            finalColumnName = "";
-        } else if (bundle == null) {
-            if (!labelName.equals(columnName)) {
-                finalColumnName = columnName + " as " + labelName;
+
+        /**
+         * Invoked when the feature type may have been found. If the given type is non-null,
+         * then this method delegates to {@link FeatureTable#setFeatureType(FeatureType)}
in
+         * the JavaFX thread. This will erase the previous content and prepare new columns.
+         *
+         * @param  type  the feature type, or {@code null}.
+         * @return whether the given type was non-null.
+         */
+        private boolean setType(final FeatureType type) {
+            if (type != null) {
+                Platform.runLater(() -> setFeatureType(type));
+                return true;
             } else {
-                finalColumnName = columnName;
+                return false;
             }
-        } else {
-            try {
-                if (!labelName.equals(columnName)) {
-                    finalColumnName = bundle.getString(columnName) + " as " + labelName;
-                } else {
-                    finalColumnName = bundle.getString(columnName);
-                }
-            } catch (MissingResourceException ex) {
-                if (!labelName.equals(columnName)) {
-                    finalColumnName = columnName + " as " + labelName;
-                } else {
-                    finalColumnName = columnName;
+        }
+    }
+
+    /**
+     * Invoked in JavaFX thread after the feature type has been determined.
+     * This method clears all rows and replaces all columns by new columns
+     * determined from the given type.
+     */
+    private void setFeatureType(final FeatureType type) {
+        assert Platform.isFxApplicationThread();
+        getItems().clear();
+        if (type != null && !type.equals(featureType)) {
+            final Collection<? extends PropertyType> properties = type.getProperties(true);
+            final List<TableColumn<Feature,?>> columns = new ArrayList<>(properties.size());
+            for (final PropertyType pt : properties) {
+                final String name = pt.getName().toString();
+                String title = string(pt.getDesignation());
+                if (title == null) {
+                    title = string(pt.getName().toInternationalString());
+                    if (title == null) title = name;
                 }
+                final TableColumn<Feature, Object> column = new TableColumn<>(title);
+                column.setCellValueFactory(new ValueGetter(name));
+                columns.add(column);
             }
+            getColumns().setAll(columns);       // Change columns in an all or nothing operation.
         }
-        return finalColumnName;
+        featureType = type;
     }
 
-    public FeatureTable(Resource res, int i) throws DataStoreException {
-        TableView<Feature> ttv = new TableView<>();
-        final ScrollPane scroll = new ScrollPane(ttv);
-        scroll.setFitToHeight(true);
-        scroll.setFitToWidth(true);
-        ttv.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
-        ttv.setTableMenuButtonVisible(true);
-        ttv.setFixedCellSize(100);
-        scroll.setPrefSize(600, 400);
-        scroll.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
-        setCenter(scroll);
-        final List<Feature> list;
-        if (res instanceof FeatureSet) {
-            try (Stream<Feature> stream = ((FeatureSet) res).features(false)) {
-                list = stream.collect(Collectors.toList());
-                ttv.setItems(FXCollections.observableArrayList(list));
-                for (PropertyType pt : list.get(0).getType().getProperties(false)) {
-                    final TableColumn<Feature, BorderPane> column = new TableColumn<>(generateFinalColumnName(pt));
-                    column.setCellValueFactory((TableColumn.CellDataFeatures<Feature,
BorderPane> param) -> {
-                        final Object val = param.getValue().getPropertyValue(pt.getName().toString());
-                        if (val instanceof Geometry) {
-                            return new SimpleObjectProperty<>(new BorderPane(new Label("{geometry}")));
-                        } else {
-                            SimpleObjectProperty<BorderPane> sop = new SimpleObjectProperty<>();
-                            if (val instanceof CheckedArrayList<?>) {
-                                Iterator<String> it = ((CheckedArrayList<String>)
val).iterator();
-                                TreeItem<String> ti = new TreeItem<>(it.next());
-                                while (it.hasNext()) {
-                                    ti.getChildren().add(new TreeItem<>(it.next()));
-                                }
-                                BorderPane bp = new BorderPane(new TreeView<>(ti));
-                                sop.setValue(bp);
-                                return sop;
-                            } else {
-                                sop.setValue(new BorderPane(new Label(String.valueOf(val))));
-                                return sop;
-                            }
-                        }
-                    });
-                    ttv.getColumns().add(column);
-                }
+    /**
+     * Fetch values to show in the table cells.
+     */
+    private static final class ValueGetter implements Callback<TableColumn.CellDataFeatures<Feature,Object>,
ObservableValue<Object>> {
+        /**
+         * The name of the feature property for which to fetch values.
+         */
+        final String name;
+
+        /**
+         * Creates a new getter of property values.
+         *
+         * @param  name  name of the feature property for which to fetch values.
+         */
+        ValueGetter(final String name) {
+            this.name = name;
+        }
+
+        /**
+         * Returns the value of the feature property wrapped by the given argument.
+         * This method is invoked by JavaFX when a new cell needs to be rendered.
+         */
+        @Override
+        public ObservableValue<Object> call(final TableColumn.CellDataFeatures<Feature,
Object> cell) {
+            Object value = cell.getValue().getPropertyValue(name);
+            if (value instanceof Collection<?>) {
+                value = "collection";               // TODO
             }
+            return new ReadOnlyObjectWrapper<>(value);
         }
     }
+
+    /**
+     * Returns the given international string as a non-empty localized string, or {@code
null} if none.
+     */
+    private String string(final InternationalString i18n) {
+        return (i18n != null) ? Strings.trimOrNull(i18n.toString(textLocale)) : null;
+    }
+
+    /**
+     * If a loading process was under way, interrupts it and close the feature stream.
+     * This method returns immediately; the release of resources happens in a background
thread.
+     */
+    public void interrupt() {
+        assert Platform.isFxApplicationThread();
+        final Loader loader = nextPageLoader;
+        nextPageLoader = null;
+        if (loader != null) {
+            loader.cancel();
+            BackgroundThreads.execute(loader::waitAndClose);
+        }
+    }
+
+    /**
+     * Reports an exception that we can not display in this widget, for example because it
applies
+     * to different data than the one currently viewed. The {@code method} argument should
be the
+     * public API (if possible) invoking the method where the exception is caught.
+     */
+    private static void unexpectedException(final String method, final Throwable exception)
{
+        Logging.unexpectedException(Logging.getLogger(Modules.APPLICATION), FeatureTable.class,
method, exception);
+    }
+
+    /**
+     * Reports an exception that we choose to ignore.
+     */
+    private static void recoverableException(final String method, final Exception exception)
{
+        Logging.recoverableException(Logging.getLogger(Modules.APPLICATION), FeatureTable.class,
method, exception);
+    }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
index 22a6b63..253ac94 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -27,6 +27,7 @@ import org.apache.sis.storage.Resource;
 import org.apache.sis.gui.metadata.MetadataSummary;
 import org.apache.sis.gui.metadata.MetadataTree;
 import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.storage.FeatureSet;
 
 
 /**
@@ -45,6 +46,11 @@ public class ResourceExplorer {
     private final ResourceTree resources;
 
     /**
+     * The data as a table.
+     */
+    private final FeatureTable features;
+
+    /**
      * The widget showing metadata about a selected resource.
      */
     private final MetadataSummary metadata;
@@ -61,21 +67,21 @@ public class ResourceExplorer {
     public ResourceExplorer() {
         resources = new ResourceTree();
         metadata  = new MetadataSummary();
+        features  = new FeatureTable();
         pane      = new SplitPane();
 
-        final MetadataTree metadataTree = new MetadataTree();
-        metadata.metadataProperty.addListener((p,o,n) -> metadataTree.setContent(n));
+        final TabPane tabs = new TabPane(
+            new Tab(resources.localized.getString(Resources.Keys.Summary),  metadata.getView()),
+            new Tab(resources.localized.getString(Resources.Keys.Data),     features),
+            new Tab(resources.localized.getString(Resources.Keys.Metadata), new MetadataTree(metadata)));
 
-        final Tab summaryTab = new Tab(resources.localized.getString(Resources.Keys.Summary),
 metadata.getView());
-        final Tab metadatTab = new Tab(resources.localized.getString(Resources.Keys.Metadata),
metadataTree);
-        final TabPane tabs = new TabPane(summaryTab, metadatTab);
         tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
         tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
 
         pane.getItems().setAll(resources, tabs);
         resources.getSelectionModel().getSelectedItems().addListener(this::selectResource);
         SplitPane.setResizableWithParent(resources, Boolean.FALSE);
-        SplitPane.setResizableWithParent(metadata.getView(), Boolean.TRUE);
+        SplitPane.setResizableWithParent(tabs, Boolean.TRUE);
         pane.setDividerPosition(0, 300);
     }
 
@@ -91,8 +97,9 @@ public class ResourceExplorer {
     }
 
     /**
-     * Adds all the given resources to the resource tree. The given collection typically
contains
-     * files to load, but may also contain {@link Resource} instances to add directly.
+     * Loads all given sources in background threads and add them to the resource tree.
+     * The given collection typically contains files to load,
+     * but may also contain {@link Resource} instances to add directly.
      * This method forwards the files to {@link ResourceTree#loadResource(Object)},
      * which will allocate a background thread for each resource to load.
      *
@@ -108,8 +115,9 @@ public class ResourceExplorer {
     }
 
     /**
-     * Invoked when a new item is selected in the resource tree.
-     * This method takes the first non-null resource and forward to the children.
+     * Invoked in JavaFX thread when a new item is selected in the resource tree.
+     * Normally, only one resource is selected since we use a single selection model.
+     * We nevertheless loop over the items as a paranoiac check and take the first non-null
resource.
      *
      * @param  change  a change event with the new resource to show.
      */
@@ -122,5 +130,6 @@ public class ResourceExplorer {
             }
         }
         metadata.setMetadata(resource);
+        features.setFeatures((resource instanceof FeatureSet) ? (FeatureSet) resource : null);
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
index 1f6d473..2838eb3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
@@ -79,7 +79,7 @@ public class MetadataSummary {
      * The locale to use for date/number formatters.
      * This is often the same than {@link #localized}.
      */
-    private final Locale formatLocale;
+    final Locale dataLocale;
 
     /**
      * The format to use for writing numbers, created when first needed.
@@ -140,9 +140,9 @@ public class MetadataSummary {
      * Creates an initially empty metadata overview.
      */
     public MetadataSummary() {
-        localized    = Resources.forLocale(Locale.getDefault(Locale.Category.DISPLAY));
-        formatLocale = Locale.getDefault(Locale.Category.FORMAT);
-        information  = new TitledPane[] {
+        localized   = Resources.forLocale(Locale.getDefault(Locale.Category.DISPLAY));
+        dataLocale  = Locale.getDefault(Locale.Category.FORMAT);
+        information = new TitledPane[] {
             new TitledPane(localized.getString(Resources.Keys.ResourceIdentification), new
IdentificationInfo(this)),
             new TitledPane(localized.getString(Resources.Keys.SpatialRepresentation),  new
RepresentationInfo(this))
         };
@@ -174,7 +174,7 @@ public class MetadataSummary {
      */
     final NumberFormat getNumberFormat() {
         if (numberFormat == null) {
-            numberFormat = NumberFormat.getInstance(formatLocale);
+            numberFormat = NumberFormat.getInstance(dataLocale);
         }
         return numberFormat;
     }
@@ -184,7 +184,7 @@ public class MetadataSummary {
      */
     final DateFormat getDateFormat() {
         if (dateFormat == null) {
-            dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM,
formatLocale);
+            dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM,
dataLocale);
         }
         return dateFormat;
     }
@@ -216,8 +216,8 @@ public class MetadataSummary {
                 /**
                  * Shows the result, unless another {@link #setMetadata(Resource)} has been
invoked.
                  */
-                @Override protected void succeeded() {if (!isCancelled()) setMetadata(getValue());}
-                @Override protected void failed()    {if (!isCancelled()) setError(getException());}
+                @Override protected void succeeded() {super.succeeded(); setMetadata(getValue());}
+                @Override protected void failed()    {super.failed();    setError(getException());}
             }
             BackgroundThreads.execute(new Getter());
         }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
index 70ce16f..e925dc3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
@@ -127,8 +127,25 @@ public class MetadataTree extends TreeTableView<TreeTable.Node>
{
      * Creates a new initially empty metadata tree.
      */
     public MetadataTree() {
-        textLocale      = Locale.getDefault(Locale.Category.DISPLAY);
-        dataLocale      = Locale.getDefault(Locale.Category.FORMAT);
+        this(null);
+    }
+
+    /**
+     * Creates a new initially empty metadata tree which will be automatically updated
+     * when the given widget shows new metadata. This constructor registers a listener
+     * to {@link MetadataSummary#metadataProperty} which forwards the metadata changes
+     * to {@link #setContent(Metadata)}.
+     *
+     * @param  controller  the widget to watch, or {@code null} if none.
+     */
+    public MetadataTree(final MetadataSummary controller) {
+        if (controller != null) {
+            textLocale = controller.localized.getLocale();
+            dataLocale = controller.dataLocale;
+        } else {
+            textLocale = Locale.getDefault(Locale.Category.DISPLAY);
+            dataLocale = Locale.getDefault(Locale.Category.FORMAT);
+        }
         contentProperty = new ContentProperty(this);
         nameColumn      = new TreeTableColumn<>(TableColumn.NAME .getHeader().toString(textLocale));
         valueColumn     = new TreeTableColumn<>(TableColumn.VALUE.getHeader().toString(textLocale));
@@ -138,6 +155,9 @@ public class MetadataTree extends TreeTableView<TreeTable.Node>
{
         setColumnResizePolicy(CONSTRAINED_RESIZE_POLICY);
         getColumns().setAll(nameColumn, valueColumn);
         contentProperty.addListener(MetadataTree::applyChange);
+        if (controller != null) {
+            controller.metadataProperty.addListener((p,o,n) -> setContent(n));
+        }
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java
index 277b0c4..10f701b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java
@@ -185,7 +185,7 @@ abstract class Section<T> extends GridPane implements EventHandler<ActionEvent>
         /*
          * Update the pane content with the first information.
          */
-        setVisible(n != 0);
+        linesEndIndex = linesStartIndex;
         if (n != 0) {
             pageGroup.selectToggle((ToggleButton) pagination.getChildren().get(0));
             update(0);
@@ -284,6 +284,6 @@ abstract class Section<T> extends GridPane implements EventHandler<ActionEvent>
      * Returns {@code true} if this section contains no data.
      */
     boolean isEmpty() {
-        return linesStartIndex == linesEndIndex;
+        return linesStartIndex >= linesEndIndex;
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index 8273092..7240f4d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -106,6 +106,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short Credit = 17;
 
         /**
+         * Data
+         */
+        public static final short Data = 32;
+
+        /**
          * Date:
          */
         public static final short Date = 18;
@@ -156,6 +161,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short Metadata = 30;
 
         /**
+         * No feature type information.
+         */
+        public static final short NoFeatureTypeInfo = 33;
+
+        /**
          * Number of dimensions:
          */
         public static final short NumberOfDimensions = 27;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index 2f88906..425bf5d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -30,6 +30,7 @@ Copy                   = Copy
 CreationDate           = Creation date:
 Credit                 = Credit:
 CRSs                   = Coordinate Reference Systems
+Data                   = Data
 Date                   = Date:
 Dimensions             = Dimensions:
 ErrorClosingFile       = Error closing file
@@ -40,6 +41,7 @@ File                   = File
 GeospatialFiles        = Geospatial data files
 Loading                = Loading\u2026
 Metadata               = Metadata
+NoFeatureTypeInfo      = No feature type information.
 NumberOfDimensions     = Number of dimensions:
 Open                   = Open\u2026
 OpenDataFile           = Open data file
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index 4aab7f8..7be1eb0 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -35,6 +35,7 @@ Copy                   = Copier
 CreationDate           = Date de cr\u00e9ation\u00a0:
 Credit                 = Cr\u00e9dit\u00a0:
 CRSs                   = Syst\u00e8mes de r\u00e9f\u00e9rence des coordonn\u00e9es
+Data                   = Donn\u00e9es
 Date                   = Date\u00a0:
 Dimensions             = Dimensions\u00a0:
 ErrorClosingFile       = Erreur \u00e0 la fermeture du fichier
@@ -45,6 +46,7 @@ File                   = Fichier
 GeospatialFiles        = Fichiers de donn\u00e9es g\u00e9ospatiales
 Loading                = Chargement\u2026
 Metadata               = Metadonn\u00e9es
+NoFeatureTypeInfo      = Pas d\u2019information sur le type d\u2019entit\u00e9.
 NumberOfDimensions     = Nombre de dimensions\u00a0:
 Open                   = Ouvrir\u2026
 OpenDataFile           = Ouvrir un fichier de donn\u00e9es
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java
b/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java
index e453faf..7ecefbb 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java
@@ -48,7 +48,7 @@ import java.sql.SQLException;
  * client code would be well advised to catch both kind of exceptions for robustness.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.3
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -115,6 +115,10 @@ public class BackingStoreException extends RuntimeException {
      *     }
      * }
      *
+     * If this exception has {@linkplain #getSuppressed() suppressed exceptions} and this
method decided
+     * that this exception should be discarded in favor of {@code <E>} or {@link RuntimeException}
cause,
+     * then this method copies the suppressed exceptions into the cause before to throw the
cause.
+     *
      * @param  <E>   the type of the exception to unwrap.
      * @param  type  the type of the exception to unwrap.
      * @return the cause as an exception of the given type (never {@code null}).
@@ -129,11 +133,27 @@ public class BackingStoreException extends RuntimeException {
     {
         final Throwable cause = getCause();
         if (type.isInstance(cause)) {
+            copySuppressed(cause);
             return (E) cause;
         } else if (cause instanceof RuntimeException) {
+            copySuppressed(cause);
             throw (RuntimeException) cause;
         } else {
             throw this;
         }
     }
+
+    /**
+     * Copies suppressed exceptions to the given target. This method is invoked before the
cause is re-thrown.
+     * Current version does not verify that this copy operation does not create duplicated
values.
+     * Most of the time, this exception has no suppressed exceptions and this method does
nothing.
+     *
+     * <p>This copy operation is useful if a {@link BackingStoreException} was thrown
inside a try-with-resource
+     * block, especially when the {@link AutoCloseable} is a {@link java.util.stream.Stream}.</p>
+     */
+    private void copySuppressed(final Throwable cause) {
+        for (final Throwable s : getSuppressed()) {
+            cause.addSuppressed(s);
+        }
+    }
 }


Mime
View raw message