All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fxmisc.flowless.SizeTracker Maven / Gradle / Ivy

There is a newer version: 0.7.3
Show newest version
package org.fxmisc.flowless;

import java.time.Duration;
import java.util.Optional;
import java.util.function.Function;

import javafx.beans.value.ObservableObjectValue;
import javafx.geometry.Bounds;
import javafx.scene.control.IndexRange;

import org.reactfx.EventStreams;
import org.reactfx.Subscription;
import org.reactfx.collection.LiveList;
import org.reactfx.collection.MemoizationList;
import org.reactfx.value.Val;
import org.reactfx.value.ValBase;

/**
 * Estimates the size of the entire viewport (if it was actually completely rendered) based on the known sizes of the
 * {@link Cell}s whose nodes are currently displayed in the viewport and an estimated average of
 * {@link Cell}s whose nodes are not displayed in the viewport. The meaning of {@link #breadthForCells} and
 * {@link #totalLengthEstimate} are dependent upon which implementation of {@link OrientationHelper} is used.
 */
final class SizeTracker {
    private final OrientationHelper orientation;
    private final ObservableObjectValue viewportBounds;
    private final MemoizationList> cells;

    private final MemoizationList breadths;
    private final Val maxKnownMinBreadth;

    /** Stores either the greatest minimum cell's node's breadth or the viewport's breadth */
    private final Val breadthForCells;

    private final MemoizationList lengths;

    /** Stores either null or the average length of the cells' nodes currently displayed in the viewport */
    private final Val averageLengthEstimate;

    private final Val totalLengthEstimate;
    private final Val lengthOffsetEstimate;

    private final Subscription subscription;

    /**
     * Constructs a SizeTracker
     *
     * @param orientation if vertical, breadth = width and length = height;
     *                    if horizontal, breadth = height and length = width
     */
    public SizeTracker(
            OrientationHelper orientation,
            ObservableObjectValue viewportBounds,
            MemoizationList> lazyCells) {
        this.orientation = orientation;
        this.viewportBounds = viewportBounds;
        this.cells = lazyCells;
        this.breadths = lazyCells.map(orientation::minBreadth).memoize();
        this.maxKnownMinBreadth = breadths.memoizedItems()
                .reduce(Math::max)
                .orElseConst(0.0);
        this.breadthForCells = Val.combine(
                maxKnownMinBreadth,
                viewportBounds,
                (a, b) -> Math.max(a, orientation.breadth(b)));

        Val, Double>> lengthFn;
        lengthFn = (orientation instanceof HorizontalHelper ? breadthForCells : avoidFalseInvalidations(breadthForCells))
                .map(breadth -> cell -> orientation.prefLength(cell, breadth));

        this.lengths = cells.mapDynamic(lengthFn).memoize();

        LiveList knownLengths = this.lengths.memoizedItems();
        Val sumOfKnownLengths = knownLengths.reduce((a, b) -> a + b).orElseConst(0.0);
        Val knownLengthCount = knownLengths.sizeProperty();

        this.averageLengthEstimate = Val.create(
                () -> {
                    // make sure to use pref lengths of all present cells
                    for(int i = 0; i < cells.getMemoizedCount(); ++i) {
                        int j = cells.indexOfMemoizedItem(i);
                        lengths.force(j, j + 1);
                    }

                    int count = knownLengthCount.getValue();
                    return count == 0
                            ? null
                            : sumOfKnownLengths.getValue() / count;
                },
                sumOfKnownLengths, knownLengthCount);

        this.totalLengthEstimate = Val.combine(
                averageLengthEstimate, cells.sizeProperty(),
                (avg, n) -> n * avg);

        Val firstVisibleIndex = Val.create(
                () -> cells.getMemoizedCount() == 0 ? null : cells.indexOfMemoizedItem(0),
                cells, cells.memoizedItems()); // need to observe cells.memoizedItems()
                // as well, because they may change without a change in cells.

        Val> firstVisibleCell = cells.memoizedItems()
                .collapse(visCells -> visCells.isEmpty() ? null : visCells.get(0));

        Val knownLengthCountBeforeFirstVisibleCell = Val.create(() -> {
            return firstVisibleIndex.getOpt()
                    .map(i -> lengths.getMemoizedCountBefore(Math.min(i, lengths.size())))
                    .orElse(0);
        }, lengths, firstVisibleIndex);

        Val totalKnownLengthBeforeFirstVisibleCell = knownLengths.reduceRange(
                knownLengthCountBeforeFirstVisibleCell.map(n -> new IndexRange(0, n)),
                (a, b) -> a + b).orElseConst(0.0);

        Val unknownLengthEstimateBeforeFirstVisibleCell = Val.combine(
                firstVisibleIndex,
                knownLengthCountBeforeFirstVisibleCell,
                averageLengthEstimate,
                (firstIdx, knownCnt, avgLen) -> (firstIdx - knownCnt) * avgLen);

        Val firstCellMinY = firstVisibleCell.flatMap(orientation::minYProperty);

        lengthOffsetEstimate = Val.wrap( EventStreams.combine(
            totalKnownLengthBeforeFirstVisibleCell.values(),
            unknownLengthEstimateBeforeFirstVisibleCell.values(),
            firstCellMinY.values()
         )
        .filter( t3 -> t3.test( (a,b,minY) -> a != null && b != null && minY != null ) )
        .thenRetainLatestFor( Duration.ofMillis( 1 ) )
        .map( t3 -> t3.map( (a,b,minY) -> Double.valueOf( a + b - minY ) ) )
        .toBinding( 0.0 ) );

        // pinning totalLengthEstimate and lengthOffsetEstimate
        // binds it all together and enables memoization
        this.subscription = Subscription.multi(
                totalLengthEstimate.pin(),
                lengthOffsetEstimate.pin());
    }

    private static  Val avoidFalseInvalidations(Val src) {
        return new ValBase() {
            @Override
            protected Subscription connect() {
                return src.observeChanges((obs, oldVal, newVal) -> invalidate());
            }

            @Override
            protected T computeValue() {
                return src.getValue();
            }
        };
    }

    public void dispose() {
        subscription.unsubscribe();
    }

    public Val maxCellBreadthProperty() {
        return maxKnownMinBreadth;
    }

    public double getViewportBreadth() {
        return orientation.breadth(viewportBounds.get());
    }

    public double getViewportLength() {
        return orientation.length(viewportBounds.get());
    }

    public Val averageLengthEstimateProperty() {
        return averageLengthEstimate;
    }

    public Optional getAverageLengthEstimate() {
        return averageLengthEstimate.getOpt();
    }

    public Val totalLengthEstimateProperty() {
        return totalLengthEstimate;
    }

    public Val lengthOffsetEstimateProperty() {
        return lengthOffsetEstimate;
    }

    public double breadthFor(int itemIndex) {
        assert cells.isMemoized(itemIndex);
        breadths.force(itemIndex, itemIndex + 1);
        return breadthForCells.getValue();
    }

    public void forgetSizeOf(int itemIndex) {
        breadths.forget(itemIndex, itemIndex + 1);
        lengths.forget(itemIndex, itemIndex + 1);
    }

    public double lengthFor(int itemIndex) {
        return lengths.get(itemIndex);
    }

    public double getCellLayoutBreadth() {
        return breadthForCells.getValue();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy