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