/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2003-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-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;
import java.awt.Color;
import java.awt.Point;
import java.awt.image.*;
import java.util.Map;
import java.util.Arrays;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import javax.media.jai.PlanarImage;
import javax.media.jai.ImageLayout;
import javax.media.jai.TileRequest;
import javax.media.jai.TileScheduler;
import javax.media.jai.TileComputationListener;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.Classes;
import org.apache.sis.util.collection.WeakValueHashMap;
import org.geotoolkit.image.color.ColorUtilities;
import org.geotoolkit.resources.Loggings;
import static java.awt.image.DataBuffer.*;
/**
* A tiled image to be used by renderer when the actual image may take a while to compute. This
* image wraps an arbitrary {@linkplain RenderedImage rendered image}, which may (or may not) be
* some image expensive to compute. When a tile is requested (through a call to {@link #getTile}
* but the tile is not available in the wrapped image, then this class returns some default
* (usually black) tile and start the real tile computation in a background thread. When the
* actual tile is available, this class fire a {@link TileObserver#tileUpdate tileUpdate} event,
* thus given a chance to a renderer to repaint again this image with the new tiles.
* <p>
* Example of use:
*
* {@preformat java
* public class Renderer extends JPanel implements TileObserver {
* private DeferredPlanarImage image;
*
* public Renderer(RenderedImage toPaint) {
* image = new DeferredPlanarImage(toPaint);
* image.addTileObserver(this);
* }
*
* public void tileUpdate(WritableRenderedImage source,
* int tileX, int tileY, boolean willBeWritable)
* {
* repaint();
* }
*
* public void paint(Graphics gr) {
* ((Graphics2D) gr).drawRenderedImage(image);
* }
* }
* }
*
* @author RĂ©mi Eve (IRD)
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @since 2.3
* @module
*/
public final class DeferredPlanarImage extends PlanarImage
implements WritableRenderedImage, TileObserver, TileComputationListener, Disposable
{
/**
* The logger for information messages.
*/
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.image");
/**
* The thickness (in pixels) of the box to draw around deferred tiles, or 0 for disabling
* this feature. Current implementation draw a box only for {@link DataBufferByte} with
* only one band.
*/
private static final int BOX_THICKNESS = 2;
/**
* An entry in the {@link #buffers} map. Contains a sample model and the sample value
* used for filling the empty {@link DataBuffer} (usually 0, unless the color model had
* a transparent pixel different from 0).
*/
private static final class Entry {
/** The sample model. */ public final SampleModel model;
/** The fill value. */ public final int fill;
/** The box value. */ public final int box;
/** Constructs a new entry. */
public Entry(final SampleModel model, final int fill, final int box) {
this.model = model;
this.fill = fill;
this.box = box;
}
/** Returns a hash code value for this entry. */
@Override
public int hashCode() {
return model.hashCode();
}
/** Compares this entry with the specified object. */
@Override
public boolean equals(final Object object) {
if (object instanceof Entry) {
final Entry that = (Entry) object;
return model.equals(that.model) && fill == that.fill && box == that.box;
}
return false;
}
}
/**
* Empty {@link DataBuffer} for a set of {@link SampleModel}.
* Will be created only when first needed.
*/
private static Map<Entry,DataBuffer> buffers;
/**
* The maximum delay (in milliseconds) to wait for a tile with one million pixels (e.g.
* a 1000×1000 tile). The actual {@link #delay} will be shorter if the tiles are
* smaller; for example the delay is four time smaller for a 500×500 tile. When
* a requested tile is not yet available, the {@link #getTile} method will wait for a
* maximum of {@code DELAY} milliseconds in case the tile computation would be very
* fast. Set the delay to 0 in order to disable this feature.
*
* {@note Must be of type <code>long</code> even if <code>int</code> would be sufficient,
* if order to force widening conversions.}
*/
private static final long DELAY = 500;
/**
* The delay (in milliseconds) to wait for a tile. When a requested tile is not yet available,
* the {@link #getTile} method will wait for a maximum of {@code delay} milliseconds in
* case the tile computation would be very fast. Set the delay to 0 in order to disable this
* feature.
*/
private final int delay;
/**
* The source image.
*/
private final PlanarImage image;
/**
* The tile observers, or {@code null} if none.
*/
private TileObserver[] observers;
/**
* The {@link TileRequest}s for a given tile.
* Tile index are computed by {@link #getTileIndex}.
* This array will be constructed only when first needed.
*/
private transient TileRequest[] requests;
/**
* Tells if a tile is request is waiting.
* Tile index are computed by {@link #getTileIndex}.
* This array will be constructed only when first needed.
*/
private transient boolean[] waitings;
/**
* Tells if a tile is in process of being computed.
* Tile index are computed by {@link #getTileIndex}.
* This array will be constructed only when first needed.
*/
private transient Raster[] pendings;
/**
* Constructs a new instance of {@code DeferredPlanarImage}.
*
* @param source The source image.
*/
public DeferredPlanarImage(final RenderedImage source) {
super(new ImageLayout(source), toVector(source), null);
image = getSourceImage(0);
image.addTileComputationListener(this);
if (image instanceof WritableRenderedImage) {
((WritableRenderedImage) image).addTileObserver(this);
}
delay = (int) Math.min(DELAY * tileWidth * tileHeight / 1000000, DELAY);
}
/**
* Wraps the specified image in a vector.
*
* @todo Should be inlined in the constructor if only Sun was to fix RFE #4093999
* ("Relax constraint on placement of this()/super() call in constructors").
*/
private static Vector<RenderedImage> toVector(final RenderedImage image) {
final Vector<RenderedImage> vector = new Vector<>(1);
vector.add(image);
return vector;
}
/**
* Returns the source images.
*/
@Override
@SuppressWarnings("unchecked")
public Vector<RenderedImage> getSources() {
return super.getSources();
}
/**
* Returns the index in {@link #requests} and {@link #pendings} array for the given tile.
* The {@code x} index varies fastest.
*/
private int getTileIndice(final int tileX, final int tileY) {
assert tileX >= getMinTileX() && tileX <= getMaxTileX() : tileX;
assert tileY >= getMinTileY() && tileY <= getMaxTileY() : tileY;
return (tileY - getMinTileY()) * getNumXTiles() +
(tileX - getMinTileX());
}
/**
* Returns the specified tile, or a default one if the requested tile is not yet available.
* If the requested tile is not immediately available, then an empty tile is returned and
* a notification will be sent later through {@link TileObserver} when the real tile will
* be available.
*
* @param tileX Tile X index.
* @param tileY Tile Y index.
* @return The requested tile.
*/
@Override
@SuppressWarnings("fallthrough")
public synchronized Raster getTile(final int tileX, final int tileY) {
if (requests == null) {
requests = new TileRequest[getNumXTiles() * getNumYTiles()];
}
final int tileIndex = getTileIndice(tileX, tileY);
TileRequest request = requests[tileIndex];
if (request == null) {
request = image.queueTiles(new Point[]{new Point(tileX, tileY)});
requests[tileIndex] = request;
}
switch (request.getTileStatus(tileX, tileY)) {
default: {
LOGGER.warning("Unknow tile status");
// Fall through
}
case TileRequest.TILE_STATUS_CANCELLED: // Fall through
case TileRequest.TILE_STATUS_FAILED: // Fall through
case TileRequest.TILE_STATUS_COMPUTED: return image.getTile(tileX, tileY);
case TileRequest.TILE_STATUS_PENDING: // Fall through
case TileRequest.TILE_STATUS_PROCESSING: break;
}
/*
* The tile is not yet available. A background thread should be computing it right
* now. Wait a little bit in case the tile computation is very fast. If we can get
* the tile in a very short time, it would be more efficient than invoking some
* 'repaint()' method later.
*/
if (pendings != null) {
if (pendings[tileIndex] != null) {
return pendings[tileIndex];
}
}
if (delay != 0) {
if (waitings == null) {
waitings = new boolean[requests.length];
}
waitings[tileIndex] = true;
try {
wait(delay);
} catch (InterruptedException exception) {
// Somebody doesn't want to lets us sleep. Go back to work.
}
waitings[tileIndex] = false;
switch (request.getTileStatus(tileX, tileY)) {
default: return image.getTile(tileX, tileY);
case TileRequest.TILE_STATUS_PENDING: // Fall through
case TileRequest.TILE_STATUS_PROCESSING: break;
}
}
/*
* The tile is not yet available and seems to take a long time to compute.
* Flag that this tile will need to be repainted later and returns an empty tile.
*/
if (LOGGER.isLoggable(Level.FINER)) {
final LogRecord record = Loggings.format(Level.FINER,
Loggings.Keys.DeferredTilePainting_2, tileX, tileY);
record.setSourceClassName(DeferredPlanarImage.class.getName());
record.setSourceMethodName("getTile");
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
}
if (pendings == null) {
pendings = new Raster[requests.length];
}
final Point origin = new Point(tileXToX(tileX), tileYToY(tileY));
final DataBuffer buffer = getDefaultDataBuffer(sampleModel, colorModel);
final Raster raster = Raster.createRaster(sampleModel, buffer, origin);
pendings[tileIndex] = raster;
fireTileUpdate(tileX, tileY, true);
return raster;
}
/**
* Returns a databuffer for the specified sample model. If the image uses an
* {@link IndexColorModel} and a {@linkplain IndexColorModel#getTransparentPixel
* transparent pixel} is defined, then raster sample values are initialized to
* the transparent pixel.
*/
private static synchronized DataBuffer getDefaultDataBuffer(
final SampleModel sampleModel, final ColorModel colorModel)
{
int fill = 0;
int box = 0;
if (colorModel instanceof IndexColorModel) {
final IndexColorModel colors = (IndexColorModel) colorModel;
fill = ColorUtilities.getTransparentPixel(colors);
if (BOX_THICKNESS > 0 && Math.min(sampleModel.getWidth(), sampleModel.getHeight()) >= 64) {
box = ColorUtilities.getColorIndex(colors, Color.DARK_GRAY, fill);
} else {
// Avoid drawing the box if tiles are too small.
box = fill;
}
}
final Entry entry = new Entry(sampleModel, fill, box);
if (buffers == null) {
buffers = new WeakValueHashMap<>(Entry.class);
}
DataBuffer buffer = buffers.get(entry);
if (buffer != null) {
return buffer;
}
/*
* No suitable data buffer existed prior to this call. Create a new one and fill it
* with the transparent color. Note that no filling is needed if the transparent value
* is 0, since the data buffer is already initialized to 0.
*/
buffer = sampleModel.createDataBuffer();
if (fill > 0) {
for (int bank=buffer.getNumBanks(); --bank>=0;) {
fill(buffer, bank, fill);
}
}
/*
* Draw a box around the tile. This is just a visual clue about tile location.
* Current implementation draw a box only for type byte with a single band.
*/
if (BOX_THICKNESS > 0 && box != fill) {
if (sampleModel.getNumBands() == 1) {
final int width = sampleModel.getWidth();
int thickness = BOX_THICKNESS;
int offset = (width + 1) * thickness;
switch (buffer.getDataType()) {
case TYPE_BYTE: {
final byte[] array = ((DataBufferByte) buffer).getData(0);
Arrays.fill(array, 0, offset, (byte) box);
Arrays.fill(array, array.length-offset, array.length, (byte) box);
thickness *= 2;
while ((offset += width) < array.length) {
Arrays.fill(array, offset-thickness, offset, (byte) box);
}
break;
}
}
}
}
buffers.put(entry, buffer);
return buffer;
}
/**
* Sets all values in the specified bank to the specified value.
*
* @param buffer The databuffer in which to set all sample values.
* @param bank Index of the bank to set.
* @param value The value.
*/
private static void fill(final DataBuffer buffer, final int bank, final int value) {
switch (buffer.getDataType()) {
case TYPE_BYTE : Arrays.fill(((DataBufferByte) buffer).getData(bank), (byte) value); break;
case TYPE_SHORT: Arrays.fill(((DataBufferShort) buffer).getData(bank), (short) value); break;
case TYPE_USHORT: Arrays.fill(((DataBufferUShort) buffer).getData(bank), (short) value); break;
case TYPE_INT: Arrays.fill(((DataBufferInt) buffer).getData(bank), value); break;
case TYPE_FLOAT: Arrays.fill(((DataBufferFloat) buffer).getData(bank), (float) value); break;
case TYPE_DOUBLE: Arrays.fill(((DataBufferDouble) buffer).getData(bank), (double) value); break;
default: throw new RasterFormatException(String.valueOf(buffer));
}
}
/**
* A tile is about to be updated (it is either about to be grabbed for writing,
* or it is being released from writing).
*/
private void fireTileUpdate(final int tileX, final int tileY, final boolean willBeWritable) {
final TileObserver[] observers = this.observers; // Avoid the need for synchronization.
if (observers != null) {
final int length = observers.length;
for (int i=0; i<length; i++) {
try {
observers[i].tileUpdate(this, tileX, tileY, willBeWritable);
} catch (RuntimeException cause) {
/*
* An exception occurred in the user code. Unfortunately, we are probably not in
* the mean user thread (e.g. the Swing thread). This method is often invoked
* from some JAI's worker thread, which we don't want to corrupt. Log a warning
* for the user and lets the JAI's worker thread continue its work.
*/
String message = cause.getLocalizedMessage();
if (message == null) {
message = Classes.getShortClassName(cause);
}
final LogRecord record = new LogRecord(Level.WARNING, message);
record.setSourceClassName(observers[i].getClass().getCanonicalName());
record.setSourceMethodName("tileUpdate");
record.setThrown(cause);
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
}
}
}
}
/**
* Invoked when a tile has been computed.
*
* @param eventSource The caller of this method.
* @param requests The relevant tile computation requests as returned by the method used to queue the tile.
* @param image The image for which tiles are being computed as specified to the {@link TileScheduler}.
* @param tileX The X index of the tile in the tile array.
* @param tileY The Y index of the tile in the tile array.
* @param tile The computed tile.
*/
@Override
public void tileComputed(final Object eventSource, final TileRequest[] requests,
final PlanarImage image, final int tileX, final int tileY, final Raster tile)
{
synchronized (this) {
final int tileIndice = getTileIndice(tileX, tileY);
if (waitings != null && waitings[tileIndice]) {
/*
* Notify the 'getTile(...)' method in only ONE thread that a tile is available.
* If tiles computation occurs in two or more background thread, then there is no
* guarantee that the notified thread is really the one waiting for this particular
* tile. However, this is not a damageable problem; the delay hint may just not be
* accuratly respected (the actual delay may be shorter for wrongly notified tile).
*/
notify();
}
if (pendings == null || pendings[tileIndice] == null) {
return;
}
pendings[tileIndice] = null;
}
fireTileUpdate(tileX, tileY, false);
}
/**
* Invoked when a tile computation has been canceled. The default implementation does nothing.
*
* @param eventSource The caller of this method.
* @param requests The relevant tile computation requests as returned by the method used to queue the tile.
* @param image The image for which tiles are being computed as specified to the {@link TileScheduler}.
* @param tileX The X index of the tile in the tile array.
* @param tileY The Y index of the tile in the tile array.
*/
@Override
public void tileCancelled(final Object eventSource, final TileRequest[] requests,
final PlanarImage image, final int tileX, final int tileY)
{
}
/**
* Invoked when a tile computation failed. Default implementation log a warning and lets the
* program continue as usual. We are not throwing an exception since this failure will alter
* the visual rendering, but will not otherwise harm the system.
*
* @param eventSource The caller of this method.
* @param requests The relevant tile computation requests as returned by the method used to queue the tile.
* @param image The image for which tiles are being computed as specified to the {@link TileScheduler}.
* @param tileX The X index of the tile in the tile array.
* @param tileY The Y index of the tile in the tile array.
* @param cause The cause of the failure.
*/
@Override
public void tileComputationFailure(final Object eventSource, final TileRequest[] requests,
final PlanarImage image, final int tileX, final int tileY, final Throwable cause)
{
final LogRecord record = new LogRecord(Level.WARNING, cause.getLocalizedMessage());
record.setSourceClassName(DeferredPlanarImage.class.getName());
record.setSourceMethodName("getTile");
record.setThrown(cause);
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
}
/**
* Invoked if the underlying image is writable and one of its tile changed.
* This method forward the call to every registered listener.
*/
@Override
public void tileUpdate(final WritableRenderedImage source,
final int tileX, final int tileY, final boolean willBeWritable)
{
fireTileUpdate(tileX, tileY, willBeWritable);
}
/**
* Adds an observer. This observer will be notified everytime a tile initially empty become
* available. If the observer is already present, it will receive multiple notifications.
*
* @param observer The observer to add.
*/
@Override
public synchronized void addTileObserver(final TileObserver observer) {
if (observer != null) {
if (observers == null) {
observers = new TileObserver[] {observer};
} else {
observers = ArraysExt.append(observers, observer);
}
}
}
/**
* Removes an observer. If the observer was not registered, nothing happens.
* If the observer was registered for multiple notifications, it will now be
* registered for one fewer.
*
* @param observer The observer to remove.
*/
@Override
public synchronized void removeTileObserver(final TileObserver observer) {
if (observers != null) {
for (int i=observers.length; --i>=0;) {
if (observers[i] == observer) {
observers = ArraysExt.remove(observers, i, 1);
break;
}
}
}
}
/**
* Checks out a tile for writing. Since {@code DeferredPlanarImage} are not really
* writable, this method throws an {@link UnsupportedOperationException}.
*/
@Override
public WritableRaster getWritableTile(final int tileX, final int tileY) {
throw new UnsupportedOperationException();
}
/**
* Relinquishes the right to write to a tile. Since {@code DeferredPlanarImage} are
* not really writable, this method throws an {@link IllegalStateException} (the state is
* really illegal since {@link #getWritableTile} should never have succeeded).
*/
@Override
public void releaseWritableTile(final int tileX, final int tileY) {
throw new IllegalStateException();
}
/**
* Returns whether any tile is checked out for writing.
*/
@Override
public synchronized boolean hasTileWriters() {
final Raster[] pendings = this.pendings;
if (pendings != null) {
final int length = pendings.length;
for (int i=0; i<length; i++) {
if (pendings[i] != null) {
return true;
}
}
}
return false;
}
/**
* Returns whether a tile is currently checked out for writing.
*/
@Override
public synchronized boolean isTileWritable(final int tileX, final int tileY) {
final Raster[] pendings = this.pendings;
return pendings != null && pendings[getTileIndice(tileX, tileY)] != null;
}
/**
* Returns an array of {@link Point} objects indicating which tiles are
* checked out for writing. Returns null if none are checked out.
*/
@Override
public synchronized Point[] getWritableTileIndices() {
final Raster[] pendings = this.pendings;
Point[] indices = null;
if (pendings != null) {
int count = 0;
final int minX = getMinTileX();
final int minY = getMinTileY();
final int numX = getNumXTiles();
final int length = pendings.length;
for (int i=0; i<length; i++) {
if (pendings[i] != null) {
if (indices == null) {
indices = new Point[length - i];
}
final int x = i % numX + minX;
final int y = i / numX + minY;
assert getTileIndice(x,y) == i : i;
indices[count++] = new Point(x,y);
}
}
if (indices != null) {
indices = ArraysExt.resize(indices, count);
}
}
return indices;
}
/**
* Sets a rectangle of the image to the contents of the raster. Since
* {@code DeferredPlanarImage} are not really writable, this method
* throws an {@link UnsupportedOperationException}.
*/
@Override
public void setData(Raster r) {
throw new UnsupportedOperationException();
}
/**
* Provides a hint that this image will no longer be accessed from a reference in user space.
* <strong>NOTE: this method dispose the image given to the constructor as well.</strong>
* This is because {@code DeferredPlanarImage} is used as a "view" of an other
* image, and the user shouldn't know that he is not using directly the other image.
*/
@Override
public synchronized void dispose() {
if (image instanceof WritableRenderedImage) {
((WritableRenderedImage) image).removeTileObserver(this);
}
image.removeTileComputationListener(this);
requests = null;
waitings = null;
pendings = null;
super.dispose();
image.dispose();
}
}