/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.image.io.large;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.Vector;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.media.jai.TileCache;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.image.iterator.PixelIterator;
import org.geotoolkit.image.iterator.PixelIteratorFactory;
import javax.imageio.spi.ImageReaderSpi;
import org.apache.sis.util.Disposable;
import org.geotoolkit.image.internal.ImageUtils;
import org.geotoolkit.image.internal.PlanarConfiguration;
import org.geotoolkit.image.internal.SampleType;
/**
* Define "Large" {@link RenderedImage} which is an image with a large size.<br/>
* It can contain more data than computer ram memory capacity, in cause of {@link TileCache}
* mechanic which store some image tiles on hard drive.
*
* TODO : Change mecanism to get a source data as entry, not a reader ? It would allow multi-threading
* on tile reading, by allocating a reader on the fly.
*
* @author Remi Marechal (Geomatys)
* @author Alexis Manin (Geomatys)
*/
public class LargeRenderedImage implements RenderedImage, Disposable {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.image.io.large");
/**
* Mechanic to store tile on hard drive.
*/
private final TileCache tilecache;
/**
* Default tile size.
*/
private static final int DEFAULT_TILE_SIZE = 256;
/**
* Minimum required tile size.
*/
private static final int MIN_TILE_SIZE = 64;
/**
* Maximum allowed tile size.
*/
private static final int MAX_TILE_SIZE = 2048;
/** Maximum dimension which will be allowed for {@link #getData(java.awt.Rectangle) } method. */
private static final Dimension RASTER_MAX_SIZE = new Dimension(10000, 10000);
/**
* The provider for {@link #imageReader}, or {@code null} if none.
*
* @see #imageReader
* @see #input
*/
private final ImageReaderSpi spi;
/**
* {@link ImageReader} where is read each image tile, or {@code null} if not yet created.
*/
private ImageReader imageReader;
/**
* {@link javax.imageio.ImageReadParam} which specify how to read the source image (subsampling, cropping).
*/
private final ImageReadParam sourceReadParam;
/**
* The input to give to {@link #imageReader}, or {@code null} is unspecified.
* This is non-null only if {@link #spi} is non-null.
*/
private final Object input;
/**
* Tile number in X direction.
*/
private final int nbrTileX;
/**
* Tile number in Y direction.
*/
private final int nbrTileY;
/**
* Define if tile will be read from {@link #imageReader} or call from {@link #tilecache}.
*/
private final boolean[][] isRead;
/**
* An array which stores a lock for each tile. The index of the tile (x, y) is retrieved as following :
* y * {@linkplain #nbrTileX} + x.
*/
private final ReentrantReadWriteLock[] tileLocks;
/**
* Image attributes.
*/
private final int imageIndex;
private final int width;
private final int height;
private final int tileWidth;
private final int tileHeight;
private final int tileGridXOffset;
private final int tileGridYOffset;
private final ColorModel cm;
private final SampleModel sm;
/**
* Create a {@link LargeRenderedImage} object with a default {@link TileCache}
* with 64 Mb memory capacity and a default tile size of 256 x 256 pixels.
*
* @param imageReader reader which target at image stored on disk.
* @param imageIndex the index of the image to be retrieved.
* @throws IOException if an error occurs during reading.
*/
public LargeRenderedImage(ImageReader imageReader, int imageIndex) throws IOException{
this(imageReader, imageIndex, null, null);
}
/**
* Create {@link LargeRenderedImage} object.
*
* @param imageReader reader which target at image stored on disk.
* @param imageIndex the index of the image to be retrieved.
* @param tilecache cache mechanic class. if {@code null} a default {@link TileCache}
* is define with a default memory capacity of 64 Mb.
* @param tileSize internal {@link Raster} (tile) dimension. if {@code null}
* a default tile size is chosen (256x256 pixels).
* @throws IOException if an error occurs during reading.
*/
public LargeRenderedImage(ImageReader imageReader, int imageIndex, TileCache tilecache, Dimension tileSize) throws IOException {
this(imageReader, null, imageIndex, tilecache, tileSize);
}
public LargeRenderedImage(ImageReader imageReader, ImageReadParam readParam, int imageIndex,
TileCache tilecache, Dimension tileSize) throws IOException
{
this(null, imageReader, readParam, null, imageIndex, tilecache, tileSize);
}
public LargeRenderedImage(ImageReaderSpi spi, ImageReadParam readParam, final Object input, int imageIndex,
TileCache tilecache, Dimension tileSize) throws IOException
{
this(spi, null, readParam, input, imageIndex, tilecache, tileSize);
}
private LargeRenderedImage(ImageReaderSpi spi, ImageReader imageReader, ImageReadParam readParam,
final Object input, int imageIndex, TileCache tilecache, Dimension tileSize) throws IOException
{
ArgumentChecks.ensurePositive("image index", imageIndex);
this.spi = spi;
this.imageReader = imageReader;
this.input = input;
this.imageIndex = imageIndex;
/* To initialize the color model, we must read a little piece of the source image.
* First, we check we have an initialized reader (containing an input), or an input along with an SPI or a reader.
*/
if ((spi == null || input == null) && (imageReader == null || (imageReader.getInput() == null && input == null))) {
throw new IllegalArgumentException("Either a valid image reader or an SPI along with an input object must " +
"be given at built.");
}
if (this.imageReader == null) {
this.imageReader = imageReader = spi.createReaderInstance();
}
if (this.imageReader.getInput() == null) {
this.imageReader.setInput(input, false, false);
}
final ImageReadParam tmpReadParam = imageReader.getDefaultReadParam();
tmpReadParam.setSourceRegion(new Rectangle(0, 0, 1, 1));
final BufferedImage tmpImage = imageReader.read(imageIndex, tmpReadParam);
cm = tmpImage.getColorModel();
if (readParam != null) {
if (readParam.getSourceRenderSize() != null) {
width = readParam.getSourceRenderSize().width;
height = readParam.getSourceRenderSize().height;
} else {
Rectangle sourceRegion = readParam.getSourceRegion();
if (sourceRegion == null) {
sourceRegion = new Rectangle(0, 0, imageReader.getWidth(imageIndex), imageReader.getHeight(imageIndex));
}
Point destOffset = readParam.getDestinationOffset();
int subsampledZoneWidth = (int) Math.ceil((double)(sourceRegion.width - readParam.getSubsamplingXOffset())/readParam.getSourceXSubsampling());
width = destOffset.x + readParam.getSubsamplingXOffset() + subsampledZoneWidth;
int subsampledZoneHeight = (int) Math.ceil((double)(sourceRegion.height - readParam.getSubsamplingYOffset())/readParam.getSourceYSubsampling());
height = destOffset.y + readParam.getSubsamplingYOffset() + subsampledZoneHeight;
}
sourceReadParam = readParam;
} else {
this.width = imageReader.getWidth(imageIndex);
this.height = imageReader.getHeight(imageIndex);
sourceReadParam = null;
}
this.tilecache = (tilecache != null) ? tilecache : LargeCache.getInstance();
this.tileGridXOffset = 0;
this.tileGridYOffset = 0;
if (tileSize != null) {
tileWidth = Math.min(Math.max(MIN_TILE_SIZE, tileSize.width), MAX_TILE_SIZE);
tileHeight = Math.min(Math.max(MIN_TILE_SIZE, tileSize.height), MAX_TILE_SIZE);
} else {
tileWidth = tileHeight = DEFAULT_TILE_SIZE;
}
//-- build a more appropriate SampleModel for this LargeRenderedImage.
final SampleModel tmpSm = tmpImage.getSampleModel();
final PlanarConfiguration pC = PlanarConfiguration.valueOf(ImageUtils.getPlanarConfiguration(tmpSm));
sm = ImageUtils.createSampleModel(pC, SampleType.valueOf(tmpSm.getDataType()), tileWidth, tileHeight, tmpSm.getNumBands());
this.nbrTileX = (width + tileWidth - 1) / tileWidth;
this.nbrTileY = (height + tileHeight - 1) / tileHeight;
isRead = new boolean[nbrTileY][nbrTileX];
for (boolean[] bool : isRead) Arrays.fill(bool, false);
tileLocks = new ReentrantReadWriteLock[nbrTileX * nbrTileY];
for (int i = 0; i < tileLocks.length; i++) {
tileLocks[i] = new ReentrantReadWriteLock();
}
}
/**
* {@inheritDoc }.
*/
@Override
public Vector<RenderedImage> getSources() {
// if (vector != null) return vector;
// vector = new Vector<RenderedImage>(numImages);
// for (int id = minIndex; id < numImages; id++) {
// try {
// vector.add(new LargeRenderedImage(imageReader, id, tilecache, new Dimension(tileWidth, tileHeight)));
// } catch (IOException ex) {
// Logging.getLogger("org.geotoolkit.image.io.large").log(Level.SEVERE, null, ex);
// }
// }
// return vector;
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public Object getProperty(String name) {
return Image.UndefinedProperty;
}
/**
* {@inheritDoc }.
*/
@Override
public String[] getPropertyNames() {
return null;
}
/**
* {@inheritDoc }.
*/
@Override
public ColorModel getColorModel() {
return cm;
}
/**
* {@inheritDoc }.
*/
@Override
public SampleModel getSampleModel() {
return sm;
}
/**
* {@inheritDoc }.
*/
@Override
public int getWidth() {
return width;
}
/**
* {@inheritDoc }.
*/
@Override
public int getHeight() {
return height;
}
/**
* {@inheritDoc }.
*/
@Override
public int getMinX() {
return 0;
}
/**
* {@inheritDoc }.
*/
@Override
public int getMinY() {
return 0;
}
/**
* {@inheritDoc }.
*/
@Override
public int getNumXTiles() {
return nbrTileX;
}
/**
* {@inheritDoc }.
*/
@Override
public int getNumYTiles() {
return nbrTileY;
}
/**
* {@inheritDoc }.
*/
@Override
public int getMinTileX() {
return (int) - (tileGridXOffset + (tileWidth - 1) * Math.signum(tileGridXOffset)) / tileWidth;
}
/**
* {@inheritDoc }.
*/
@Override
public int getMinTileY() {
return (int) - (tileGridYOffset + (tileHeight - 1) * Math.signum(tileGridYOffset)) / tileHeight;
}
/**
* {@inheritDoc }.
*/
@Override
public int getTileWidth() {
return tileWidth;
}
/**
* {@inheritDoc }.
*/
@Override
public int getTileHeight() {
return tileHeight;
}
/**
* {@inheritDoc }.
*/
@Override
public int getTileGridXOffset() {
return tileGridXOffset;
}
/**
* {@inheritDoc }.
*/
@Override
public int getTileGridYOffset() {
return tileGridYOffset;
}
/**
* {@inheritDoc }.
*/
@Override
public Raster getTile(int tileX, int tileY) {
final ReadWriteLock tileLock = tileLocks[tileY * nbrTileX + tileX];
tileLock.readLock().lock();
try {
if (isRead[tileY][tileX]) {
return tilecache.getTile(this, tileX, tileY);
}
} catch (IllegalArgumentException e) {
/*
* Should occurs if LargeCache is used in memory mode only and the requested tile
* is not anymore in cache.
*/
LOGGER.log(Level.FINER, "Tile not found in cache system.", e);
isRead[tileY][tileX] = false;
} catch (Exception e) {
/* This block is because of possible runtime exception if there's a cache problem,
* we don't throw error, just reload the tile.
*/
LOGGER.log(Level.FINE, "Cannot get tile from cache system, but it should be here !", e);
} finally {
tileLock.readLock().unlock();
}
// Prepare for tile loading
tileLock.writeLock().lock();//-- lock about boolean array isRead[]
try {
try {
if (isRead[tileY][tileX]) {
return tilecache.getTile(this, tileX, tileY);
}
} catch (Exception e) {
// Do not log again, it must have been done above.
}
// Compute tile position in source image
final int minRx = tileX * tileWidth;
final int minRy = tileY * tileHeight;
int tileWidth = Math.min(minRx + this.tileWidth, width) - minRx;
int tileHeight = Math.min(minRy + this.tileHeight, height) - minRy;
final ImageReadParam imgParam = imageReader.getDefaultReadParam();
final BufferedImage result;
// no subsampling nor offset, read directly the specified region.
if (sourceReadParam == null) {
imgParam.setSourceRegion(new Rectangle(minRx, minRy, tileWidth, tileHeight));
ImageReader reader = imageReader.getOriginatingProvider().createReaderInstance();
try{
reader.setInput(imageReader.getInput());
result = reader.read(imageIndex, imgParam);
}finally{
reader.dispose();
}
} else {
/* If an offset has been specified, we must fill result only from this point. First, we check if the given tile
* is completely before the destination offset, in which case we just have to return a black filled image.
* Otherwise, we compute the source region to read which intersects the asked tile rectangle.
*/
final Point destOffset = sourceReadParam.getDestinationOffset();
if (minRx + tileWidth < destOffset.x || minRy + tileHeight < destOffset.y) {
result = new BufferedImage(cm, cm.createCompatibleWritableRaster(tileWidth, tileHeight), cm.isAlphaPremultiplied(), null);
} else {
if (minRx < destOffset.x || minRy < destOffset.y) {
imgParam.setDestination(new BufferedImage(cm, cm.createCompatibleWritableRaster(tileWidth, tileHeight), cm.isAlphaPremultiplied(), null));
imgParam.setDestinationOffset(new Point(Math.max(0, destOffset.x - minRx), Math.max(0, destOffset.y - minRy)));
}
final Rectangle srcRegion = sourceReadParam.getSourceRegion();
final int ssX = sourceReadParam.getSourceXSubsampling();
final int ssY = sourceReadParam.getSourceYSubsampling();
final int readOffsetX = minRx-destOffset.x;
final int readOffsetY = minRy-destOffset.y;
// Put subsampling offset only on left and upper border tiles.
imgParam.setSourceRegion(new Rectangle(
srcRegion.x + (readOffsetX > 0? readOffsetX * ssX : sourceReadParam.getSubsamplingXOffset()),
srcRegion.y + (readOffsetY > 0? readOffsetY * ssY : sourceReadParam.getSubsamplingYOffset()),
(tileWidth + Math.min(0, readOffsetX)) * ssX,
(tileHeight + Math.min(0, readOffsetY)) * ssY));
imgParam.setSourceSubsampling(ssX, ssY, 0, 0);
ImageReader reader = imageReader.getOriginatingProvider().createReaderInstance();
try{
reader.setInput(imageReader.getInput());
result = reader.read(imageIndex, imgParam);
}finally{
reader.dispose();
}
}
}
final WritableRaster wRaster = Raster.createWritableRaster(result.getSampleModel(), result.getRaster().getDataBuffer(), new Point(minRx, minRy));
tilecache.add(this, tileX, tileY, wRaster);
isRead[tileY][tileX] = true;
return wRaster;
} catch (IOException e) {
throw new IllegalStateException("Impossible to read tile from image reader.", e);
} finally {
tileLock.writeLock().unlock();
}
}
/**
* {@inheritDoc }.
*/
@Override
public Raster getData() {
// in contradiction with this class aim.
// in attempt to replace JAI dependencies.
if (width <= RASTER_MAX_SIZE.width && height <= RASTER_MAX_SIZE.height) {
final WritableRaster wr = Raster.createWritableRaster(cm.createCompatibleSampleModel(width, height), new Point(0, 0));
final Rectangle rect = new Rectangle();
int my = 0;
for (int ty = 0, tmy = 0 + nbrTileY; ty < tmy; ty++) {
int mx = 0;
for (int tx = 0, tmx = 0 + nbrTileX; tx < tmx; tx++) {
final Raster r = getTile(tx, ty);
rect.setBounds(mx, my, tileWidth, tileHeight);
//recopie
final PixelIterator copix = PixelIteratorFactory.createDefaultWriteableIterator(wr, wr, rect);
final PixelIterator pix = PixelIteratorFactory.createDefaultIterator(r, rect);
while (copix.next()) {
pix.next();
copix.setSampleDouble(pix.getSampleDouble());
}
mx += tileWidth;
}
my += tileHeight;
}
return wr;
}
throw new UnsupportedOperationException(String.format("Image width/height exceed max size (%d/%d).",
RASTER_MAX_SIZE.width, RASTER_MAX_SIZE.height));
}
/**
* {@inheritDoc }.
*/
@Override
public Raster getData(Rectangle rect) {
// in contradiction with this class aim.
// in attempt to replace JAI dependencies.
final int minX = 0;
final int minY = 0;
final int minTileGridY = 0;
final int minTileGridX = 0;
final int rx = Math.max(rect.x, minX);
final int ry = Math.max(rect.y, minY);
final int rw = Math.min(rect.x+rect.width, minX+width)-rx;
final int rh = Math.min(rect.y+rect.height, minY+height)-ry;
if (rw <= RASTER_MAX_SIZE.width && rh <= RASTER_MAX_SIZE.height) {
final WritableRaster wr = Raster.createWritableRaster(cm.createCompatibleSampleModel(rw, rh), new Point(rx, ry));
final Rectangle area = new Rectangle();
int ty = minTileGridY + (ry - minY) / tileHeight;
int tbx = minTileGridX + (rx - minX) / tileWidth;
int tmaxY = (ry+rh-minY+tileHeight-1)/tileHeight;
int tmaxX = (rx+rw-minX+tileWidth-1)/tileWidth;
for (; ty < tmaxY; ty++) {
for (int tx = tbx; tx < tmaxX; tx++) {
final Raster r = getTile(tx, ty);
final int ix = Math.max(rx, minX + (tx-minTileGridX) * tileWidth);
final int iy = Math.max(ry, minY + (ty-minTileGridY) * tileHeight);
final int imx = Math.min(rx + rw, minX + (tx + 1 - minTileGridX) * tileWidth);
final int imy = Math.min(ry + rh, minY + (ty + 1 - minTileGridY) * tileHeight);
area.setBounds(ix, iy, imx-ix, imy-iy);
//recopie
final PixelIterator copix = PixelIteratorFactory.createDefaultWriteableIterator(wr, wr, area);
final PixelIterator pix = PixelIteratorFactory.createDefaultIterator(r, area);
while (copix.next()) {
pix.next();
copix.setSampleDouble(pix.getSampleDouble());
}
}
}
return wr;
}
throw new UnsupportedOperationException(String.format("Image width/height exceed max size (%d/%d).",
RASTER_MAX_SIZE.width, RASTER_MAX_SIZE.height));
}
/**
* {@inheritDoc }.
*/
@Override
public WritableRaster copyData(WritableRaster raster) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void dispose() {
tilecache.removeTiles(this);
if (spi != null && imageReader != null) {
imageReader.dispose();
imageReader = null;
}
}
/**
* {@inheritDoc}.
*/
@Override
protected void finalize() throws Throwable {
dispose();
super.finalize();
}
}