/*
* 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.util.Arrays;
import java.util.Vector;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.image.iterator.PixelIterator;
import org.geotoolkit.image.iterator.PixelIteratorFactory;
/**
*
* @author Alexis Manin (Geomatys).s
* @author Remi Marechal (Geomatys).
*/
public class WritableLargeRenderedImage implements WritableRenderedImage {
private final LargeCache tilecache;
/**
* Default tile size.
*/
private static final int DEFAULT_TILE_SIZE = 256;
/**
* Minimum required tile size.
*/
private static final int MIN_TILE_SIZE = 64;
/**
* Upper left corner of all image tiles.
*/
private Point[] tileIndices = null;
/**
* Defaine if tile is allready writen.
*/
private final boolean[][] isWrite;
/**
* 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 attributs.
*/
private final int minX;
private final int minY;
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 int minTileGridX;
private int minTileGridY;
private final int nbrTileX;
private final int nbrTileY;
private final ColorModel cm;
private final SampleModel sm;
/**
* Create {@link WritableLargeRenderedImage} with default upper corner at position (0, 0),
* a default tile size of 256 x 256 pixels and a default tile grid offset at position (0, 0).
*
* @param width image width.
* @param height image height.
* @param colorModel {@link ColorModel} use to build {@link WritableRaster} (image tiles).
*/
public WritableLargeRenderedImage(int width, int height, ColorModel colorModel, SampleModel sampleModel) {
this(0, 0, width, height, null, 0, 0, colorModel, sampleModel);
}
/**
* Create {@link WritableLargeRenderedImage} object.
*
* @param minX image upper left corner min X values.
* @param minY image upper left corner min Y values.
* @param width image width.
* @param height image height.
* @param tileSize size of tile or raster within this image.
* @param tileGridXOffset tile grid offset in X direction.
* @param tileGridYOffset tile grid offset in Y direction.
* @param colorModel {@link ColorModel} use to build {@link WritableRaster} (image tiles).
*/
public WritableLargeRenderedImage(int minX, int minY, int width, int height,
Dimension tileSize, int tileGridXOffset, int tileGridYOffset,
ColorModel colorModel, SampleModel sm) {
ArgumentChecks.ensureNonNull("ColorModel", colorModel);
ArgumentChecks.ensureStrictlyPositive("image width", width);
ArgumentChecks.ensureStrictlyPositive("image height", height);
this.tilecache = LargeCache.getInstance();
this.minX = minX;
this.minY = minY;
this.width = width;
this.height = height;
if (tileSize == null) {
this.tileWidth = this.tileHeight = DEFAULT_TILE_SIZE;
} else {
tileWidth = Math.min(Math.max(MIN_TILE_SIZE, tileSize.width), DEFAULT_TILE_SIZE);
tileHeight = Math.min(Math.max(MIN_TILE_SIZE, tileSize.height), DEFAULT_TILE_SIZE);
}
this.tileGridXOffset = tileGridXOffset;
this.tileGridYOffset = tileGridYOffset;
this.nbrTileX = (width + tileWidth - 1) / tileWidth;
this.nbrTileY = (height + tileHeight - 1) / tileHeight;
this.cm = colorModel;
this.sm = sm;
this.minTileGridX = (minX - tileGridXOffset) / tileWidth;
this.minTileGridY = (minY - tileGridYOffset) / tileHeight;
if (tileGridXOffset < minX) minTileGridX--;
if (tileGridYOffset < minY) minTileGridY--;
isWrite = new boolean[nbrTileY][nbrTileX];
for (boolean[] bool : isWrite) Arrays.fill(bool, false);
//-- tile lock initialize
tileLocks = new ReentrantReadWriteLock[nbrTileX * nbrTileY];
for (int i = 0; i < tileLocks.length; i++) {
tileLocks[i] = new ReentrantReadWriteLock();
}
}
/**
* {@inheritDoc }.
*/
@Override
public void addTileObserver(TileObserver to) {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public void removeTileObserver(TileObserver to) {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public WritableRaster getWritableTile(int tileX, int tileY) {
return (WritableRaster) getTile(tileX, tileY);
}
/**
* {@inheritDoc }.
*/
@Override
public void releaseWritableTile(int tileX, int tileY) {
}
/**
* {@inheritDoc }.
*/
@Override
public boolean isTileWritable(int tileX, int tileY) {
return (getTile(tileX, tileY) instanceof WritableRaster);
}
/**
* {@inheritDoc }.
*/
@Override
public Point[] getWritableTileIndices() {
if (tileIndices == null) {
tileIndices = new Point[nbrTileX*nbrTileY];
int idTab = 0;
for (int ty = minTileGridY, tmy = minTileGridY+nbrTileY; ty < tmy; ty++) {
for (int tx = minTileGridX, tmx = minTileGridX+nbrTileX; tx < tmx; tx++) {
tileIndices[idTab++] = new Point(tx, ty);
}
}
}
return tileIndices;
}
/**
* {@inheritDoc }.
*/
@Override
public boolean hasTileWriters() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public void setData(Raster r) {
final int rminX = r.getMinX();
final int rminY = r.getMinY();
final int rw = r.getWidth();
final int rh = r.getHeight();
final int rmaxX = rminX+rw;
final int rmaxY = rminY+rh;
if (Math.abs(minX-rminX) % tileWidth != 0) {
throw new IllegalArgumentException("raster minX value don't tie in tile coordinate");
}
if (Math.abs(minY-rminY) % tileHeight != 0) {
throw new IllegalArgumentException("raster minY value don't tie in tile coordinate");
}
final int ix = Math.max(rminX, minX);
final int imx = Math.min(rmaxX, minX+width);
final int iy = Math.max(rminY, minY);
final int imy = Math.min(rmaxY, minY+height);
if (imx <= ix || imy <= iy) {
throw new IllegalArgumentException("raster is not within image boundary");
}
if ((imx-ix) != rw || (imy-iy) != rh) {
throw new IllegalArgumentException("raster boundary don't tie in tile coordinate");
}
if (r.getSampleModel().getDataType() != sm.getDataType()) {
throw new IllegalArgumentException("raster datatype don't tie with image datatype");
}
final int tx = minTileGridX + (rminX-minX) / tileWidth;
final int ty = minTileGridY + (rminY-minY) / tileHeight;
final int isRow = ty - minTileGridY;
final int isCol = tx - minTileGridX;
final ReentrantReadWriteLock lock = tileLocks[ty * nbrTileX + tx];
lock.writeLock().lock(); //-- ask reading token
try {
if (isWrite[isRow][isCol]) tilecache.remove(this, tx, ty);
tilecache.add(this, tx, ty, r);
isWrite[isRow][isCol] = true;
} finally {
lock.writeLock().unlock();
}
}
/**
* {@inheritDoc }.
*/
@Override
public Vector<RenderedImage> getSources() {
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 minX;
}
/**
* {@inheritDoc }.
*/
@Override
public int getMinY() {
return minY;
}
/**
* {@inheritDoc }.
*/
@Override
public int getNumXTiles() {
return nbrTileX;
}
/**
* {@inheritDoc }.
*/
@Override
public int getNumYTiles() {
return nbrTileY;
}
/**
* {@inheritDoc }.
*/
@Override
public int getMinTileX() {
return minTileGridX;
}
/**
* {@inheritDoc }.
*/
@Override
public int getMinTileY() {
return minTileGridY;
}
/**
* {@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 ReentrantReadWriteLock lock = tileLocks[tileY * nbrTileX + tileX];
lock.readLock().lock(); //-- ask reading token
final boolean iswrite;
try {
iswrite = isWrite[tileY - minTileGridY][tileX - minTileGridX];
} finally {
lock.readLock().unlock();
}
if (!iswrite) fillWritableImage(tileX, tileY);//-- inside this method : writing lock
return tilecache.getTile(this, tileX, tileY);
}
/**
* {@inheritDoc }.
*/
@Override
public Raster getData() {
// in contradiction with this class aim.
// in attempt to replace JAI dependencies.
if (width <= 5000 && height <= 5000) {
final WritableRaster wr = Raster.createWritableRaster(cm.createCompatibleSampleModel(width, height), new Point(minX, minY));
final Rectangle rect = new Rectangle();
int my = minY;
for (int ty = minTileGridY, tmy = minTileGridY + nbrTileY; ty < tmy; ty++) {
int mx = minX;
for (int tx = minTileGridX, tmx = minTileGridX + nbrTileX; tx < tmx; tx++) {
final Raster r = tilecache.getTile(this, 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("Not supported yet. Raster weight too expensive.");
}
/**
* {@inheritDoc }.
*/
@Override
public Raster getData(Rectangle rect) {
// in contradiction with this class aim.
// in attempt to replace JAI dependencies.
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 <= 5000 && rh <= 5000) {
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 = tilecache.getTile(this, 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("Not supported yet.Raster weight too expensive.");
}
/**
* {@inheritDoc }.
*/
@Override
public WritableRaster copyData(WritableRaster raster) {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
protected void finalize() throws Throwable {
tilecache.removeTiles(this);
super.finalize();
}
/**
* Create an original {@link Raster} adapted for image properties.
*
* @param tileX image tile index in X direction.
* @param tileY image tile index in Y direction.
*/
private void fillWritableImage(final int tileX, final int tileY) {
final ReentrantReadWriteLock lock = tileLocks[tileY * nbrTileX + tileX];
lock.writeLock().lock(); //-- ask reading token
try {
if (!isWrite[tileY - minTileGridY][tileX - minTileGridX]) {
//-- upper left raster corner.
final int ptOffx = minX + (tileX - minTileGridX) * tileWidth;
final int ptOffy = minY + (tileY - minTileGridY) * tileHeight;
//-- raster width and height at least.
final int rw = StrictMath.min(ptOffx + tileWidth, minX + width) - ptOffx;
final int rh = StrictMath.min(ptOffy + tileHeight, minY + height) - ptOffy;
this.tilecache.add(this, tileX, tileY, Raster.createWritableRaster(cm.createCompatibleSampleModel(rw, rh), new Point(ptOffx, ptOffy)));
isWrite[tileY - minTileGridY][tileX - minTileGridX] = true;
}
} finally {
lock.writeLock().unlock();
}
//// //remplissage de raster vide
//// final int mx = minX + width;
//// final int my = minY + height;
//// int ry = minY;
//// for (int ty = minTileGridY, tmy = minTileGridY+nbrTileY; ty < tmy; ty++){
//// int rx = minX;
//// int row = ty - minTileGridY;
//// for (int tx = minTileGridX, tmx = minTileGridX+nbrTileX; tx < tmx; tx++) {
//// int col = tx - minTileGridX;
//// if (!isWrite[row][col]) {
//// final int rw = Math.min(rx + tileWidth, mx) - rx;
//// final int rh = Math.min(ry + tileHeight, my) - ry;
//// ptOffset.setLocation(rx, ry);
//// this.tilecache.add(this, tx, ty, Raster.createWritableRaster(cm.createCompatibleSampleModel(rw, rh), ptOffset));
//// isWrite[row][col] = true;
//// }
//// rx += tileWidth;
//// }
//// ry += tileHeight;
//// }
}
}