sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Load only a small number of features, with more features loaded only when needed.
Date Wed, 06 Nov 2019 15:54:01 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 77d3a6399967cabaf34e1d663e86f229fec41ac1
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Nov 6 16:49:49 2019 +0100

    Load only a small number of features, with more features loaded only when needed.
---
 .../org/apache/sis/gui/dataset/FeatureList.java    | 258 +++++++++++++++++++++
 .../org/apache/sis/gui/dataset/FeatureLoader.java  | 233 ++++++++++++++++---
 .../org/apache/sis/gui/dataset/FeatureTable.java   | 194 ++--------------
 .../gazetteer/MilitaryGridReferenceSystem.java     |   6 +
 .../sis/internal/netcdf/impl/FeaturesInfo.java     |   4 +-
 .../apache/sis/internal/sql/feature/Features.java  |   7 +-
 6 files changed, 481 insertions(+), 221 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureList.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureList.java
new file mode 100644
index 0000000..2c384f3
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureList.java
@@ -0,0 +1,258 @@
+/*
+ * 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.dataset;
+
+import java.util.List;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Spliterator;
+import javafx.application.Platform;
+import javafx.collections.ObservableListBase;
+import org.opengis.feature.Feature;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * An observable list of features containing only a subset of {@link FeatureSet} content.
+ * When an element is requested, if that element has not yet been read, the reading is done
+ * in a background thread.
+ *
+ * <p>This list does not accept null elements; any attempt to add a null feature is
silently ignored.
+ * The null value is reserved for meaning that the element is in process of being loaded.</p>
+ *
+ * <p>All methods in this class shall be invoked from JavaFX thread.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class FeatureList extends ObservableListBase<Feature> {
+    /**
+     * The elements in this list, never {@code null}.
+     */
+    private Feature[] elements;
+
+    /**
+     * Number of valid elements in {@link #elements}.
+     */
+    private int validCount;
+
+    /**
+     * Expected number of elements. Can not be smaller than {@link #validCount}.
+     * May be greater than {@link #elements} length if some elements are not yet loaded.
+     */
+    private int estimatedSize;
+
+    /**
+     * Whether {@link #estimatedSize} is exact.
+     */
+    private boolean isSizeExact;
+
+    /**
+     * If not all features have been read, the task for loading the next batch
+     * of {@value FeatureLoader#PAGE_SIZE} features in a background thread.
+     * 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>
+     *
+     * @see #setNextPage(FeatureLoader)
+     */
+    private FeatureLoader nextPageLoader;
+
+    /**
+     * Creates a new list of features.
+     */
+    FeatureList() {
+        elements = new Feature[0];
+    }
+
+    /**
+     * Schedules a background thread which will set the features in this list.
+     * If the loading of another {@code FeatureSet} was in progress at the
+     * time this method is invoked, that previous loading is cancelled.
+     *
+     * @param  table     the table which own this list.
+     * @param  features  the features to show in the table, or {@code null} if none.
+     * @return whether a background process has been scheduled.
+     */
+    final boolean setFeatures(final FeatureTable table, final FeatureSet features) {
+        assert Platform.isFxApplicationThread();
+        final FeatureLoader previous = nextPageLoader;
+        if (previous != null) {
+            nextPageLoader = null;
+            previous.cancel();
+        }
+        if (features != null) {
+            nextPageLoader = new FeatureLoader.Initial(table, features);
+            BackgroundThreads.execute(nextPageLoader);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Invoked by {@link FeatureLoader} for replacing the current content by a new list of
features.
+     * The list size after this method invocation will be {@code expectedSize}, not {@code
count}.
+     * The missing elements will be implicitly null until {@link #addFeatures(Feature[],
int)} is invoked.
+     * If the expected size is unknown (i.e. its value is {@link Long#MAX_VALUE}),
+     * then an arbitrary size is computed from {@code count}.
+     *
+     * @param  remainingCount   value of {@link Spliterator#estimateSize()} after partial
traversal.
+     * @param  characteristics  value of {@link Spliterator#characteristics()}.
+     * @param  features         new features. This array is not cloned and may be modified
in-place.
+     * @param  count            number of valid elements in the given array.
+     */
+    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
+    final void setFeatures(long remainingCount, int characteristics, final Feature[] features,
final int count) {
+        assert Platform.isFxApplicationThread();
+        int newValidCount = 0;
+        for (int i=0; i<count; i++) {
+            final Feature f = features[i];
+            if (f != null) features[newValidCount++] = f;       // Exclude null elements.
+        }
+        final List<Feature> removed = Arrays.asList(elements);  // Want this call outside
{beginChange … endChange}.
+        if (remainingCount == Long.MAX_VALUE) {
+            remainingCount    = count + 10L;                    // Arbitrary additional amount.
+            characteristics = 0;
+        }
+        estimatedSize = (int) Math.min(Integer.MAX_VALUE, Math.addExact(remainingCount, newValidCount));
+        isSizeExact   = (characteristics & Spliterator.SIZED) != 0;
+        elements      = features;
+        validCount    = newValidCount;
+        beginChange();
+        nextReplace(0, estimatedSize, removed);
+        endChange();
+    }
+
+    /**
+     * Invoked when more features have been loaded. This method does not actually changes
the size of
+     * this list, unless the number of elements after this method call exceeds {@link #estimatedSize}.
+     *
+     * @param  features  the features to add. Null elements are ignored.
+     * @param  count     number of valid elements in the given array.
+     * @throws ArithmeticException if the number of elements exceeds this list capacity.
+     */
+    final void addFeatures(final Feature[] features, final int count) {
+        assert Platform.isFxApplicationThread();
+        if (count > 0) {
+            int newValidCount = Math.addExact(validCount, count);
+            if (newValidCount > elements.length) {
+                // Note: if `length << 1` overflows, it will be negative and max(…)
= newValidCount.
+                elements = Arrays.copyOf(elements, Math.max(newValidCount, elements.length
<< 1));
+            }
+            newValidCount = validCount;         // Recompute `validCount + count` but excluding
null elements.
+            for (int i=0; i<count; i++) {
+                final Feature f = features[i];
+                if (f != null) elements[newValidCount++] = f;
+            }
+            /*
+             * This method is not really adding new elements, but replacing null elements
by non-null elements.
+             * Only if the new size exceeds the previously expected size, we send a notification
about addition.
+             */
+            final int replaceTo = Math.min(newValidCount, estimatedSize);
+            final List<Feature> removed = Collections.nCopies(replaceTo - validCount,
null);
+            if (newValidCount > estimatedSize) {
+                estimatedSize = newValidCount;              // Update before we send events.
+            }
+            beginChange();
+            nextReplace(validCount, replaceTo, removed);
+            nextAdd(replaceTo, validCount = newValidCount);
+            endChange();
+        }
+    }
+
+    /**
+     * Sets the task to be used for next features to load. A {@code null} values notifies
+     * this list that the loading process is finished and no more elements will be added.
+     *
+     * @param  next  the loader for next {@value FeatureLoader#PAGE_SIZE} features,
+     *               or {@code null} if there is no more features to load.
+     */
+    final void setNextPage(final FeatureLoader next) {
+        assert Platform.isFxApplicationThread();
+        nextPageLoader = next;
+        if (next == null) {
+            final int n = estimatedSize - validCount;
+            if (n != 0) {
+                final List<Feature> removed = Collections.nCopies(n, null);
+                estimatedSize = validCount;
+                beginChange();
+                nextRemove(validCount, removed);
+                endChange();
+            }
+            isSizeExact = true;
+            elements = ArraysExt.resize(elements, validCount);
+        }
+    }
+
+    /**
+     * Returns whether the specified loader is the one scheduled for loading next page of
features.
+     * We use this check in case a loader has been cancelled and another one started its
work immediately.
+     */
+    final boolean isCurrentLoader(final FeatureLoader loader) {
+        return loader == nextPageLoader;
+    }
+
+    /**
+     * Returns the estimated number of elements.
+     * Note that this value may be greater than the number of elements actually loaded.
+     */
+    @Override
+    public int size() {
+        return estimatedSize;
+    }
+
+    /**
+     * Returns the element at the given index. If the element is expected to exist
+     * but has not yet been loaded, returns {@code null}.
+     */
+    @Override
+    public Feature get(final int index) {
+        assert Platform.isFxApplicationThread();
+        if (index < validCount) {
+            return elements[index];
+        }
+        if (isSizeExact && index >= estimatedSize) {
+            throw new IndexOutOfBoundsException(index);
+        }
+        if (nextPageLoader != null) {
+            BackgroundThreads.execute(nextPageLoader);
+        }
+        return 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.
+     *
+     * @see FeatureTable#interrupt()
+     */
+    final void interrupt() {
+        assert Platform.isFxApplicationThread();
+        final FeatureLoader loader = nextPageLoader;
+        nextPageLoader = null;
+        if (loader != null) {
+            loader.cancel();
+            BackgroundThreads.execute(loader::waitAndClose);
+        }
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureLoader.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureLoader.java
index 3f6032c..ba11213 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureLoader.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureLoader.java
@@ -16,41 +16,50 @@
  */
 package org.apache.sis.gui.dataset;
 
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Spliterator;
 import java.util.stream.Stream;
+import java.util.function.Consumer;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
+import javafx.application.Platform;
 import javafx.concurrent.Task;
 import org.opengis.feature.Feature;
-import org.apache.sis.storage.DataStoreException;
+import org.opengis.feature.FeatureType;
 import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.internal.gui.Resources;
 
 
 /**
  * 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.
+ * The boolean value returned by this task tells whether there is more features to load.
  *
- * <p>Loading processes are started by {@link org.apache.sis.gui.dataset.FeatureTable.InitialLoader}.
- * Only additional pages are loaded by ordinary {@code Loader}.</p>
+ * <p>Loading processes are started by {@link Initial} loader.
+ * Only additional pages are loaded by ordinary {@code FeatureLoader}.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
-class FeatureLoader extends Task<List<Feature>> {
+class FeatureLoader extends Task<Boolean> implements Consumer<Feature> {
     /**
      * Maximum number of features to load in a background task.
      * If there is more features to load, we will use many tasks.
      *
-     * @see FeatureTable#nextPageLoader
+     * @see FeatureList#nextPageLoader
      */
     private static final int PAGE_SIZE = 100;
 
     /**
+     * The table where to add the features loaded by this task.
+     * All methods on this object shall be invoked from JavaFX thread.
+     */
+    private final FeatureTable table;
+
+    /**
      * The stream to close after we finished to iterate over features.
      * This stream should not be used for any other purpose.
      */
@@ -62,9 +71,16 @@ class FeatureLoader extends Task<List<Feature>> {
     private Spliterator<Feature> iterator;
 
     /**
-     * An estimation of the number of features, or {@link Long#MAX_VALUE} if unknown.
+     * The features loaded by this task. This array is created in a background thread,
+     * then added to {@link #table} in the JavaFX thread.
      */
-    private long estimatedCount;
+    private Feature[] loaded;
+
+    /**
+     * Number of features loaded by this task.
+     * This is the number of valid elements in the {@link #loaded} array.
+     */
+    private int count;
 
     /**
      * Creates a new loader. Callers shall invoke {@link #initialize(FeatureSet)}
@@ -72,8 +88,8 @@ class FeatureLoader extends Task<List<Feature>> {
      *
      * @see #initialize(FeatureSet)
      */
-    FeatureLoader() {
-        estimatedCount = Long.MAX_VALUE;
+    FeatureLoader(final FeatureTable table) {
+        this.table = table;
     }
 
     /**
@@ -82,48 +98,55 @@ class FeatureLoader extends Task<List<Feature>> {
      * {@link #call()} execution.
      */
     final void initialize(final FeatureSet features) throws DataStoreException {
-        toClose        = features.features(false);
-        iterator       = toClose .spliterator();
-        estimatedCount = iterator.estimateSize();
+        toClose  = features.features(false);
+        iterator = toClose.spliterator();
     }
 
     /**
      * Creates a new task for continuing the work of a previous task.
      * The new task will load the next {@value #PAGE_SIZE} features.
-     *
-     * @see #next()
      */
     private FeatureLoader(final FeatureLoader previous) {
-        toClose        = previous.toClose;
-        iterator       = previous.iterator;
-        estimatedCount = previous.estimatedCount;
+        table    = previous.table;
+        toClose  = previous.toClose;
+        iterator = previous.iterator;
     }
 
     /**
-     * If there is more features to load, returns a new task for loading the next
-     * {@value #PAGE_SIZE} features. Otherwise returns {@code null}.
+     * Returns the list where to add features.
+     * All methods on the returned list shall be invoked from JavaFX thread.
      */
-    final FeatureLoader next() {
-        return (iterator != null) ? new FeatureLoader(this) : null;
+    private FeatureList destination() {
+        return (FeatureList) table.getItems();
+    }
+
+    /**
+     * Callback method for {@link Spliterator#tryAdvance(Consumer)},
+     * defined for {@link #call()} internal purpose only.
+     */
+    @Override
+    public void accept(final Feature feature) {
+        loaded[count++] = feature;
     }
 
     /**
      * Invoked in a background thread for loading up to {@value #PAGE_SIZE} features.
      * If this method completed successfully but there is still more feature to read,
-     * then {@link #iterator} will have a non-null value and {@link #next()} should be
-     * invoked for preparing the reading of another page of features. In other cases,
+     * then {@link #iterator} will keep a non-null value and a new {@link FeatureLoader}
+     * should be prepared for reading of another page of features. In other cases,
      * {@link #iterator} is null and the stream has been closed.
+     *
+     * @return whether there is more features to load.
      */
     @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().
+    protected Boolean call() throws DataStoreException {
+        // Note: iterator.estimateSize() is a count or remaining elements.
+        final int stopAt = (int) Math.min(iterator.estimateSize(), PAGE_SIZE);
+        loaded = new Feature[stopAt];
+        try {
+            while (iterator.tryAdvance(this)) {
+                if (count >= stopAt) {
+                    return Boolean.TRUE;                // Intentionally skip the call to
close().
                 }
                 if (isCancelled()) {
                     break;
@@ -138,7 +161,7 @@ class FeatureLoader extends Task<List<Feature>> {
             throw e.unwrapOrRethrow(DataStoreException.class);
         }
         close();                                        // Loading completed or has been
cancelled.
-        return instances;
+        return Boolean.FALSE;
     }
 
     /**
@@ -146,7 +169,7 @@ class FeatureLoader extends Task<List<Feature>> {
      * but only when {@link #call()} finished its work (if unsure, see {@link #waitAndClose()}).
      * It is safe to invoke this method again even if this loader has already been closed.
      */
-    final void close() throws DataStoreException {
+    private void close() throws DataStoreException {
         iterator = null;
         final Stream<Feature> c = toClose;
         if (c != null) try {
@@ -188,4 +211,142 @@ class FeatureLoader extends Task<List<Feature>> {
             FeatureTable.unexpectedException("interrupt", error);
         }
     }
+
+    /**
+     * Invoked in JavaFX thread after new feature instances are ready.
+     * This method adds the new rows in the table and prepares another
+     * task for loading the next batch of features when needed.
+     */
+    @Override
+    protected final void succeeded() {
+        final FeatureList addTo = destination();
+        if (addTo.isCurrentLoader(this)) {
+            if (this instanceof Initial) {
+                addTo.setFeatures(iterator.estimateSize(), iterator.characteristics(), loaded,
count);
+            } else {
+                addTo.addFeatures(loaded, count);
+            }
+            addTo.setNextPage(getValue() ? new FeatureLoader(this) : null);
+        } else try {
+            close();
+        } catch (DataStoreException e) {
+            FeatureTable.unexpectedException("setFeatures", e);
+        }
+    }
+
+    /**
+     * Invoked in JavaFX thread when a loading process has been cancelled or failed.
+     * This method closes the {@link FeatureLoader} if it did not closed itself,
+     * then eventually shows the error in the table area.
+     *
+     * @see FeatureTable#interrupt()
+     */
+    @Override
+    protected final void cancelled() {
+        final FeatureList addTo = destination();
+        final boolean isCurrentLoader = addTo.isCurrentLoader(this);
+        if (isCurrentLoader) {
+            addTo.setNextPage(null);
+        }
+        /*
+         * 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.
+         */
+        Throwable exception = getException();
+        try {
+            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.
+                FeatureTable.unexpectedException("cancelled", exception);
+            }
+        }
+    }
+
+    /**
+     * Invoked in JavaFX thread when a loading process failed.
+     */
+    @Override
+    protected final void failed() {
+        cancelled();
+    }
+
+    /**
+     * 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 FeatureLoader} will be used.
+     */
+    static final class Initial extends FeatureLoader {
+        /**
+         * The set of features to read.
+         */
+        private final FeatureSet features;
+
+        /**
+         * Initializes a new task for loading features from the given set.
+         */
+        Initial(final FeatureTable table, final FeatureSet features) {
+            super(table);
+            this.features = features;
+        }
+
+        /**
+         * Gets the feature type, initializes 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}.
+         */
+        @Override
+        protected Boolean call() throws DataStoreException {
+            final boolean isTypeKnown = setType(features.getType());
+            initialize(features);
+            final Boolean status = super.call();
+            if (isTypeKnown) {
+                setTypeFromFirst();
+            }
+            return status;
+        }
+    }
+
+    /**
+     * 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.
+     */
+    final boolean setType(final FeatureType type) {
+        if (type != null) {
+            Platform.runLater(() -> table.setFeatureType(type));
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Safety for data stores that do not implement the {@link FeatureSet#getType()} method.
+     * That method is mandatory and implementations 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.
+     */
+    final void setTypeFromFirst() throws DataStoreException {
+        for (int i=0; i<count; i++) {
+            final Feature f = loaded[i];
+            if (f != null && setType(f.getType())) {
+                return;
+            }
+        }
+        throw new DataStoreException(Resources.forLocale(table.textLocale).getString(Resources.Keys.NoFeatureTypeInfo));
+    }
 }
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 80151d0..909cf88 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
@@ -20,12 +20,10 @@ import java.util.Locale;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Collection;
-import javafx.application.Platform;
 import javafx.scene.control.TableColumn;
 import javafx.scene.control.TableView;
 import javafx.beans.property.ReadOnlyObjectWrapper;
 import javafx.beans.value.ObservableValue;
-import javafx.concurrent.WorkerStateEvent;
 import javafx.util.Callback;
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
@@ -33,11 +31,8 @@ import org.opengis.feature.PropertyType;
 import org.opengis.util.InternationalString;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.storage.FeatureSet;
-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.storage.DataStoreException;
 
 
 /**
@@ -60,7 +55,7 @@ public class FeatureTable extends TableView<Feature> {
     /**
      * The locale to use for texts.
      */
-    private final Locale textLocale;
+    final Locale textLocale;
 
     /**
      * The locale to use for dates/numbers.
@@ -77,16 +72,6 @@ public class FeatureTable extends TableView<Feature> {
     private FeatureType featureType;
 
     /**
-     * If not all features have been read, the task for loading the next batch
-     * of {@value FeatureLoader#PAGE_SIZE} features in a background thread.
-     * 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 FeatureLoader nextPageLoader;
-
-    /**
      * Creates an initially empty table.
      */
     public FeatureTable() {
@@ -94,6 +79,7 @@ public class FeatureTable extends TableView<Feature> {
         dataLocale = Locale.getDefault(Locale.Category.FORMAT);
         setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
         setTableMenuButtonVisible(true);
+        setItems(new FeatureList());
     }
 
     /**
@@ -110,170 +96,20 @@ public class FeatureTable extends TableView<Feature> {
      * @param  features  the features to show in this table, or {@code null} if none.
      */
     public void setFeatures(final FeatureSet features) {
-        assert Platform.isFxApplicationThread();
-        final FeatureLoader previous = nextPageLoader;
-        if (previous != null) {
-            nextPageLoader = null;
-            previous.cancel();
-        }
-        if (features != null) {
-            prepare(new InitialLoader(features));
-            BackgroundThreads.execute(nextPageLoader);
-        } else {
+        final FeatureList items = (FeatureList) getItems();
+        if (!items.setFeatures(this, features)) {
             featureType = null;
-            getItems().clear();
+            items.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 FeatureLoader#PAGE_SIZE} features,
-     *                 or {@code null} if there is no more features to load.
-     */
-    private void prepare(final FeatureLoader loader) {
-        if (loader != null) {
-            loader.setOnSucceeded(this::addFeatures);
-            loader.setOnCancelled(this::cancelled);
-            loader.setOnFailed   (this::cancelled);
-        }
-        nextPageLoader = loader;
-    }
-
-    /**
-     * Invoked in JavaFX thread after new feature instances are ready.
-     * This method adds the new rows in the table and prepares another
-     * task for loading the next batch of features when needed.
-     */
-    private void addFeatures(final WorkerStateEvent event) {
-        assert Platform.isFxApplicationThread();
-        final FeatureLoader loader = (FeatureLoader) event.getSource();
-        if (loader == nextPageLoader) {
-            getItems().addAll((List<Feature>) event.getSource().getValue());
-            prepare(nextPageLoader.next());
-
-            // 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);
-        }
-    }
-
-    /**
-     * Invoked in JavaFX thread when a loading process has been cancelled or failed.
-     * This method closes the {@link FeatureLoader} if it did not closed itself,
-     * then eventually shows the error in the table area.
-     *
-     * @see #interrupt()
-     */
-    private void cancelled(final WorkerStateEvent event) {
-        assert Platform.isFxApplicationThread();
-        final FeatureLoader loader = (FeatureLoader) event.getSource();
-        final boolean isCurrentLoader = (loader == nextPageLoader);
-        if (isCurrentLoader) {
-            nextPageLoader = null;
-        }
-        /*
-         * 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.
-         */
-        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);
-            }
-        }
-    }
-
-    /**
-     * 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 FeatureLoader} will be used.
-     */
-    private final class InitialLoader extends FeatureLoader {
-        /**
-         * 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, initializes 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}.
-         */
-        @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;
-            }
-            /*
-             * Following code is a safety for FeatureSet that do not implement the `getType()`
method.
-             * That method is mandatory and implementations 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.
-             */
-            for (final Feature f : instances) {
-                if (f != null && setType(f.getType())) {
-                    return instances;
-                }
-            }
-            throw new DataStoreException(Resources.forLocale(textLocale).getString(Resources.Keys.NoFeatureTypeInfo));
-        }
-
-        /**
-         * 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 {
-                return false;
-            }
-        }
-    }
-
-    /**
      * 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();
+    final void setFeatureType(final FeatureType type) {
         getItems().clear();
         if (type != null && !type.equals(featureType)) {
             final Collection<? extends PropertyType> properties = type.getProperties(true);
@@ -318,9 +154,13 @@ public class FeatureTable extends TableView<Feature> {
          */
         @Override
         public ObservableValue<Object> call(final TableColumn.CellDataFeatures<Feature,
Object> cell) {
-            Object value = cell.getValue().getPropertyValue(name);
-            if (value instanceof Collection<?>) {
-                value = "collection";               // TODO
+            Object value = null;
+            final Feature feature = cell.getValue();
+            if (feature != null) {
+                value = feature.getPropertyValue(name);
+                if (value instanceof Collection<?>) {
+                    value = "collection";               // TODO
+                }
             }
             return new ReadOnlyObjectWrapper<>(value);
         }
@@ -338,13 +178,7 @@ public class FeatureTable extends TableView<Feature> {
      * This method returns immediately; the release of resources happens in a background
thread.
      */
     public void interrupt() {
-        assert Platform.isFxApplicationThread();
-        final FeatureLoader loader = nextPageLoader;
-        nextPageLoader = null;
-        if (loader != null) {
-            loader.cancel();
-            BackgroundThreads.execute(loader::waitAndClose);
-        }
+        ((FeatureList) getItems()).interrupt();
     }
 
     /**
diff --git a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
index eae965e..97eaeb9 100644
--- a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
+++ b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
@@ -807,6 +807,9 @@ public class MilitaryGridReferenceSystem extends ReferencingByIdentifiers
{
             /**
              * Guess the number of elements to be returned. The value returned by this method
is very rough,
              * and likely greater than the real amount of elements that will actually be
returned.
+             *
+             * <p><b>Note:</b> returned value should be the number of <em>remaining</em>
elements, but
+             * current implementation does not compute how many elements we have already
traversed.</p>
              */
             @Override
             public long estimateSize() {
@@ -1141,6 +1144,9 @@ public class MilitaryGridReferenceSystem extends ReferencingByIdentifiers
{
          * Returns an estimation of the number of cells in the area covered by this iterator.
The returned value
          * may be greater than the real amount since we do not take in account the fact that
the number of cells
          * in a row become lower as we approach poles.
+         *
+         * <p><b>Note:</b> returned value should be the number of <em>remaining</em>
elements, but
+         * current implementation does not compute how many elements we have already traversed.</p>
          */
         @Override
         public long estimateSize() {
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
index 8f30f9d..81d9283 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
@@ -402,11 +402,11 @@ search: for (final VariableInfo counts : decoder.variables) {
         }
 
         /**
-         * Returns the number of features.
+         * Returns the remaining number of features to traverse.
          */
         @Override
         public long estimateSize() {
-            return counts.size();
+            return counts.size() - index;
         }
 
         /**
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
index 57e9c1f..fd04b4b 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
@@ -135,9 +135,9 @@ final class Features implements Spliterator<Feature>, Runnable {
     private final Class<?> keyComponentClass;
 
     /**
-     * Estimated number of rows, or {@literal <= 0} if unknown.
+     * Estimated number of remaining rows, or {@literal <= 0} if unknown.
      */
-    private final long estimatedSize;
+    private long estimatedSize;
 
     /**
      * Creates a new iterator over the feature instances.
@@ -311,7 +311,7 @@ final class Features implements Spliterator<Feature>, Runnable {
     }
 
     /**
-     * Returns the estimated number of features, or {@link Long#MAX_VALUE} if unknown.
+     * Returns the estimated number of remaining features, or {@link Long#MAX_VALUE} if unknown.
      */
     @Override
     public long estimateSize() {
@@ -362,6 +362,7 @@ final class Features implements Spliterator<Feature>, Runnable {
      */
     private boolean fetch(final Consumer<? super Feature> action, final boolean all)
throws SQLException {
         while (result.next()) {
+            estimatedSize--;
             final Feature feature = featureType.newInstance();
             for (int i=0; i < attributeNames.length; i++) {
                 final Object value = result.getObject(i+1);


Mime
View raw message