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 <[email protected]> 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); + } + } }
