/*
* Copyright (C) 2015 Patryk Strach
*
* This file is part of Virtual Slide Viewer.
*
* Virtual Slide Viewer is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*
* Virtual Slide Viewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Virtual Slide Viewer.
* If not, see <http://www.gnu.org/licenses/>.
*/
package virtualslideviewer.imageviewing;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import virtualslideviewer.core.BufferedVirtualSlideImage;
import virtualslideviewer.core.ImageIndex;
import virtualslideviewer.core.Tile;
import virtualslideviewer.core.VirtualSlideImage;
import virtualslideviewer.util.ByteArrayPool;
import virtualslideviewer.util.ImageUtil;
import virtualslideviewer.util.ParameterValidator;
/**
* Asynchronous loader for extracting a visible part from virtual slide image.
* It uses threading, caching and prefetching to provide the best performance and is highly configurable.
*/
public class AsyncVisibleImageLoader implements VisibleImageLoader
{
private final ExecutorService mThreadPool;
private final TileLoadingPrioritizer mLoadingPrioritizer;
private final PrefetchingStrategy mPrefetchingStrategy;
private final LoadingTilePlaceholderGenerator mPlaceholderGenerator;
private final ByteArrayPool mTempTileDataBufferPool = new ByteArrayPool();
private final List<Future<?>> mPreviouslyLoadingFutures = new ArrayList<>();
/**
* @param tileLoadingExecutor A thread pool which the loader will use to schedule tile loading tasks.
* @param placeholderGenerator A data generator for tiles which has been not loaded yet.
* @param prefetchingStrategy A strategy to prefetch not visible yet but possibly soon needed tiles.
* @param loadingPrioritizer A prioritizer to select which parts of the image should be loaded first.
*/
public AsyncVisibleImageLoader(ExecutorService tileLoadingExecutor, LoadingTilePlaceholderGenerator placeholderGenerator,
PrefetchingStrategy prefetchingStrategy, TileLoadingPrioritizer loadingPrioritizer)
{
ParameterValidator.throwIfNull(tileLoadingExecutor, "tileLoadingExecutor");
ParameterValidator.throwIfNull(placeholderGenerator, "placeholderGenerator");
ParameterValidator.throwIfNull(prefetchingStrategy, "prefetchingStrategy");
ParameterValidator.throwIfNull(loadingPrioritizer, "loadingPrioritizer");
mThreadPool = tileLoadingExecutor;
mPlaceholderGenerator = placeholderGenerator;
mPrefetchingStrategy = prefetchingStrategy;
mLoadingPrioritizer = loadingPrioritizer;
}
/**
* Asynchronously loads visible part of pixels from the image.
*
* The pixels are loaded in their original form if they are in cache, otherwise they data is loaded in background
* and data generated by the tile placeholder generator will be returned in the place of real data until its in cache.
*
* After the data is loaded, user specified callback will be called and real data can be retrieved by calling this function again.
*
* Calling this function stops any not yet finished loading tasks started in previous call.
*
* @param image Image from which visible part will be loaded.
* @param dst Buffer which will receive already the data.
* @param visibleImageBounds The bounds of currently visible image.
* @param imageIndex The image index from which to retrieve data.
* @param dataUpdatedCallback A callback which will be called once for every part of an image when is it made available.
* The callback will be called from a <b>different thread</b>.
*/
@Override
public void getVisibleImageData(BufferedVirtualSlideImage image, byte[] dst, Rectangle visibleImageBounds, ImageIndex imageIndex,
Runnable dataUpdatedCallback)
{
validateArguments(image, dst, visibleImageBounds, imageIndex, dataUpdatedCallback);
cancelPreviousLoading();
loadDataInto(image, dst, visibleImageBounds, imageIndex, dataUpdatedCallback);
prefetchTiles(image, visibleImageBounds, imageIndex);
}
private void validateArguments(BufferedVirtualSlideImage image, byte[] dst, Rectangle visibleImageBounds, ImageIndex imageIndex,
Runnable dataUpdatedCallback)
{
ParameterValidator.throwIfNull(image, "image");
ParameterValidator.throwIfNull(dst, "dst");
ParameterValidator.throwIfNull(visibleImageBounds, "visibleImageBounds");
ParameterValidator.throwIfNull(dataUpdatedCallback, "dataUpdatedCallback");
if(!imageIndex.isValid(image))
throw new IllegalArgumentException("Invalid image index.");
if(isNotASubImage(image, visibleImageBounds, imageIndex.getResolutionIndex()))
throw new IllegalArgumentException("Invalid visible image bounds.");
int minimumBufferSize = visibleImageBounds.width * visibleImageBounds.height * (image.isRGB() ? 3 : 1);
if(dst.length < minimumBufferSize)
{
throw new IllegalArgumentException("Destination buffer is too small. Capacity of at least " +
minimumBufferSize + " bytes is needed.");
}
}
private boolean isNotASubImage(VirtualSlideImage image, Rectangle subImageBounds, int resIndex)
{
Rectangle fullImageBounds = new Rectangle(new Point(0, 0), image.getImageSize(resIndex));
return !fullImageBounds.contains(subImageBounds);
}
private void cancelPreviousLoading()
{
for(Future<?> taskHandle : mPreviouslyLoadingFutures)
{
taskHandle.cancel(false);
}
mPreviouslyLoadingFutures.clear();
}
private void prefetchTiles(BufferedVirtualSlideImage image, Rectangle visibleImageBounds, ImageIndex imageIndex)
{
for(Tile tile : mPrefetchingStrategy.getTilesToPrefetch(image, visibleImageBounds, imageIndex))
{
if(!image.isImageInCache(tile.getBounds(image), imageIndex))
{
Future<?> taskHandle = mThreadPool.submit(() -> image.ensureTileDataCached(tile));
mPreviouslyLoadingFutures.add(taskHandle);
}
}
}
private void loadDataInto(BufferedVirtualSlideImage image, byte[] dst, Rectangle visibleImageBounds, ImageIndex imageIndex,
Runnable dataUpdatedCallback)
{
List<Tile> tilesToLoad = ImageUtil.getTilesInArea(visibleImageBounds, image.getTileSize(imageIndex.getResolutionIndex()), imageIndex);
mLoadingPrioritizer.sortTilesByPriority(tilesToLoad, image, visibleImageBounds);
byte[] tempTileBuffer = mTempTileDataBufferPool.borrow(getRequiredTileBufferSize(image, imageIndex.getResolutionIndex()));
{
for(Tile tile : tilesToLoad)
{
getTileData(image, tempTileBuffer, tile, dataUpdatedCallback);
int imageChannelCount = (image.isRGB() ? 3 : 1);
ImageUtil.copyIntersectingPartOfImage(tempTileBuffer, tile.getBounds(image),
dst, visibleImageBounds, imageChannelCount);
}
}
mTempTileDataBufferPool.putBack(tempTileBuffer);
}
private int getRequiredTileBufferSize(VirtualSlideImage image, int resIndex)
{
Dimension fullTileSize = image.getTileSize(resIndex);
return fullTileSize.width * fullTileSize.height * (image.isRGB() ? 3 : 1);
}
private void getTileData(BufferedVirtualSlideImage image, byte[] dst, Tile tile, Runnable dataUpdatedCallback)
{
if(image.isImageInCache(tile.getBounds(image), tile.getImageIndex()))
{
image.getTileData(dst, tile);
return;
}
startTileLoading(image, tile, dataUpdatedCallback);
mPlaceholderGenerator.getTilePlaceholder(dst, image, tile);
}
private void startTileLoading(BufferedVirtualSlideImage image, Tile tile, Runnable dataUpdatedCallback)
{
Future<?> taskHandle = mThreadPool.submit(() ->
{
image.ensureTileDataCached(tile);
dataUpdatedCallback.run();
});
mPreviouslyLoadingFutures.add(taskHandle);
}
}