blob: f5dc70e198058cb61d46dcf0a7e64d86591e1a72 [file] [log] [blame]
/*
* 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 javafx.concurrent.Worker;
import org.apache.sis.storage.FeatureSet;
import org.apache.sis.gui.internal.BackgroundThreads;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.privy.UnmodifiableArrayList;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
/**
* 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 is unmodifiable through public API except for the {@link #clear()} method.
* This list should be modified only through package-private API.
* The intent is to prevents uncontrolled modifications to introduce inconsistencies
* with the modifications to be applied by the loader running in background thread.</p>
*
* <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 only.</p>
*
* @todo Current implementation does not release previously loaded features.
* We could do that in a future version if memory usage is a problem,
* provided that {@link Spliterator#ORDERED} is set.
*
* @author Martin Desruisseaux (Geomatys)
*/
final class FeatureList extends ObservableListBase<Feature> {
/**
* Number of empty rows to show in the bottom of the table when we don't know how many rows still
* need to be read. Those rows do not stay empty for long since they will become valid as soon as
* the background loader finished to load a page ({@value FeatureLoader#PAGE_SIZE} rows).
*/
private static final long NUM_PENDING_ROWS = 10;
/**
* Maximum number of rows that this list will allow. Must be smaller than {@link Integer#MAX_VALUE}.
*/
private static final int MAXIMUM_ROWS = Integer.MAX_VALUE - 1;
/**
* The {@link #elements} value when this list is empty.
*/
private static final Feature[] EMPTY = new Feature[0];
/**
* 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. Cannot 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 = EMPTY;
}
/**
* Returns the currently valid elements.
*/
private List<Feature> validElements() {
return UnmodifiableArrayList.wrap(elements, 0, validCount);
}
/**
* Clears the content of this list. While this method can be invoked from public API,
* it should be reserved to {@link FeatureList} and {@link FeatureTable} internal usage.
* This method should be invoked only when no loader is running in a background thread,
* or when the loaded decided itself to invoke this method.
*/
@Override
public void clear() {
final List<Feature> removed = validElements();
elements = EMPTY;
estimatedSize = 0;
validCount = 0;
beginChange();
nextReplace(0, 0, removed);
endChange();
}
/**
* 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 startFeaturesLoading(final FeatureTable table, final FeatureSet features) {
assert Platform.isFxApplicationThread();
final FeatureLoader previous = nextPageLoader;
if (previous != null) {
nextPageLoader = null;
previous.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO);
}
if (features != null) {
nextPageLoader = new FeatureLoader(table, features);
BackgroundThreads.execute(nextPageLoader);
return true;
} else {
clear();
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, boolean)}
* 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.
* @param hasMore if the stream may have more features.
*/
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
final void setFeatures(long remainingCount, int characteristics,
final Feature[] features, final int count, final boolean hasMore)
{
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 = validElements(); // Want this call outside {beginChange … endChange}.
if (remainingCount == Long.MAX_VALUE) {
remainingCount = count + NUM_PENDING_ROWS; // Arbitrary additional amount.
characteristics = 0;
}
estimatedSize = (int) Math.min(MAXIMUM_ROWS, Math.addExact(remainingCount, newValidCount));
isSizeExact = (characteristics & Spliterator.SIZED) != 0;
elements = features;
validCount = newValidCount;
if (hasMore && estimatedSize <= newValidCount) {
// Estimated size seems incorrect. Add some empty rows for triggering a new page load.
estimatedSize = (int) Math.min(MAXIMUM_ROWS, newValidCount + NUM_PENDING_ROWS);
}
beginChange();
nextReplace(0, estimatedSize, removed);
endChange();
checkOverflow();
}
/**
* 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.
* @param hasMore if the stream may have more features.
* @throws ArithmeticException if the number of elements exceeds this list capacity.
*/
final void addFeatures(final Feature[] features, final int count, final boolean hasMore) {
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.
if (hasMore) {
// Estimated size seems incorrect. Add some empty rows for triggering a new page load.
estimatedSize = (int) Math.min(MAXIMUM_ROWS, newValidCount + NUM_PENDING_ROWS);
}
}
beginChange();
nextReplace(validCount, replaceTo, removed);
nextAdd(replaceTo, replaceTo + (validCount = newValidCount));
endChange();
checkOverflow();
}
}
/**
* If we cannot load more features stop the reading process.
*
* @todo Add some message in the widget for warning the user.
* Proposal: set MAXIMUM_ROWS to MAX_INTEGER - 2 and reserve the last table row for a message.
* That row would span all columns. That row could also be used for exception message when the
* exception did not happened at the file beginning.
*/
private void checkOverflow() {
if (validCount >= MAXIMUM_ROWS) {
interrupt();
}
}
/**
* 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 are no more features to load.
*/
final void setNextPage(final FeatureLoader next) {
assert Platform.isFxApplicationThread();
assert nextPageLoader.isDone();
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);
}
final FeatureLoader loader = nextPageLoader;
if (loader != null && loader.getState() == Worker.State.READY) {
BackgroundThreads.execute(loader);
}
return null;
}
/**
* If a loading process was under way, interrupts it and closes 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.NO_INTERRUPT_DURING_IO);
BackgroundThreads.execute(loader::waitAndClose);
}
}
}