/*
 * $RCSfile: OpImage.java,v $
 *
 * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * $Revision: 1.1 $
 * $Date: 2005/02/11 04:57:12 $
 * $State: Exp $
 */
package com.lightcrafts.mediax.jai;

import com.lightcrafts.media.jai.util.ImageUtil;
import com.lightcrafts.media.jai.util.JDKWorkarounds;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.image.ColorModel; // 3-22-00 used in deprecated methods only
import java.awt.image.IndexColorModel; // 3-22-00 used in deprecated mthds only
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel; // 3-22-00 used in deprecated methods only
import java.awt.image.WritableRaster;
import java.util.Map;
import java.util.Vector;

/**
 * This is the base class for all image operations.  It provides a home
 * for information and functionalities common to all the op-image classes,
 * and implements various utility methods that may be useful to a specific
 * operation.  Image operations may be divided into different categories
 * based on their characteristics.  A subclass, extending
 * <code>OpImage</code>, represents a category and implements methods
 * unique and common to those operations.  Each individual operator
 * should extend the subclass that represents the specific
 * category that operator belongs to.
 *
 * <p> The layout variables of an <code>OpImage</code> are inherited from the
 * <code>PlanarImage</code> superclass.  The layout should be set when the
 * <code>OpImage</code> is constructed.  Each subclass must set
 * the appropriate layout variables and supply them via the
 * <code>ImageLayout</code> argument at construction time.  This class
 * simply modifies these settings as described in the <code>OpImage</code>
 * constructor comments before forwarding the layout to the
 * <code>PlanarImage</code> constructor.  If a subclass needs to modify
 * any of the layout settings subsequent to invoking its superclass
 * constructors it should use the <code>setImageLayout()</code> method
 * defined in <code>PlanarImage</code> in preference to setting the
 * layout variables directly.
 *
 * <p> A <code>RenderedImage</code>'s pixel data type and number of bands
 * are defined by its <code>SampleModel</code>, while the
 * <code>ColorModel</code> translates the pixel data into color/alpha
 * components in the specific <code>ColorSpace</code> that is associated
 * with the <code>ColorModel</code>.
 *
 * <p> By default, the operators provided by Java Advanced Imaging (JAI)
 * operate on the image's pixel data only.  That is, the computations
 * are performed on the data described by the image's
 * <code>SampleModel</code>.  No color translation is performed prior
 * to the actual computation by the operator, regardless of the type of
 * the <code>ColorModel</code> an image has.  If a user intends to have
 * an operation performed on the color data, he must perform the
 * color translation explicitly prior to invoking the operation.
 *
 * <p> There are those operators that specifically deal with the
 * color/alpha data of an image.  Such an operator must state its
 * behavior in its <code>OperationDescriptor</code> explicitly and
 * explain its intended usage of the image's color/alpha component data.
 * In such cases, the image's <code>ColorModel</code> as well as the
 * associated <code>ColorSpace</code> should be considered.
 *
 * <p> However there are certain operations, the results of which are
 * incorrect when the source has colormapped imagery, i.e. the source
 * has an <code>IndexColorModel</code>, and the computations are
 * performed on the image's non color transformed pixel data. In JAI,
 * such operations are those that are implemented as subclasses of
 * {@link AreaOpImage}, {@link GeometricOpImage}, and the "format" 
 * operation. These operations set the 
 * {@link JAI#KEY_REPLACE_INDEX_COLOR_MODEL} <code>RenderingHint</code>
 * to true, thus ensuring that the operations are performed correctly 
 * on the colormapped imagery, not treating the indices into the color
 * map as pixel data. 
 *
 * <p> The tile cache and scheduler are handled by this class.  JAI
 * provides a default implementation for <code>TileCache</code> and
 * <code>TileScheduler</code>.  However, they may be overriden by
 * each application.  An <code>OpImage</code> may share a common cache
 * with other <code>OpImage</code>s, or it may have a private cache of
 * its own.  To override an existing cache, use the
 * <code>setTileCache</code> method; an input argument of <code>null</code>
 * indicates that this image should not have a tile cache.
 *
 * <p> The <code>getTile</code> method may be used to request a tile
 * of the image.  The default implementation of this method in this
 * class first checks whether the requested tile is in the tile cache,
 * and if not, uses the default <code>TileScheduler</code> to schedule
 * the tile for computation.  Once the tile has been computed, it is
 * added to the cache and returned as a <code>Raster</code>.
 *
 * <p> The JAI tile scheduler assumes that when a request is made to
 * schedule a tile for computation via the <code>scheduleTile</code>
 * method, that tile is not currently in the cache.  To avoid a cycle,
 * it calls <code>OpImage.computeTile</code> for the actual tile
 * computation.
 *
 * <p> The default implementation of the <code>computeTile</code> method 
 * in this class first creates a new <code>Raster</code> to represent
 * the requested tile, then calls one of the two <code>computeRect</code>
 * methods to compute the actual pixel values and store the result in
 * the <code>DataBuffer</code> of the <code>Raster</code>.
 *
 * <p> Two variants of the <code>computeRect</code> method exist.
 *
 * <p> The first (with input arguments <code>Raster[]</code>,
 * <code>WritableRaster</code>, and <code>Rectangle</code>) is used when
 * the <code>OpImage</code> is constructed with the
 * <code>cobbleSources</code> argument set to <code>true</code>.
 * This indicates that the source data must be cobbled into a single
 * <code>Raster</code> and that all the necessary source data are provided
 * in order to compute the rectangular region of the destination image.
 * The source <code>Raster</code> array contains one entry for each
 * source image.
 *
 * <p> The second (with input arguments <code>PlanarImage[]</code>,
 * <code>WritableRaster</code>, and <code>Rectangle</code>) is used when
 * the <code>OpImage</code> is constructed with the
 * <code>cobbleSources</code> argument set to <code>false</code>.
 * This indicates that the source data are not cobbled into a single
 * <code>Raster</code>; instead an array of <code>PlanarImage</code>s,
 * one for each source, supply the source data and each image is
 * responsible for performing its own data accesses.  This variant is
 * generally useful if iterators are to be used for the underlying
 * implementation of accessing the image data.
 *
 * <p> The two <code>computeRect</code> methods are not abstract because
 * normally only one needs to be implemented by the subclass depending on
 * the <code>cobbleSources</code> value.  The default implementation of
 * these two methods in this class throws a <code>RuntimeException</code>.
 *
 * <p> Every operator who follows the above default implementation must
 * supply an overridden version of at least one of the
 * <code>computeRect</code> method variants, and specify which one is
 * to be called via the <code>cobbleSources</code> argument of the
 * constructor, or an exception will be thrown at run time.
 *
 * <p> If a subclass overrides <code>getTile</code> not to call
 * <code>computeTile</code>, does not use the JAI implementation of
 * <code>TileScheduler</code>, overrides <code>computeTile</code> not to
 * call <code>computeRect</code>, or does not follow the above default
 * implementation in any way, then it may need to handle issues such as
 * tile caching, multi-threading, and etc. by itself and may not need to
 * override some of the methods described above.  In some cases, some of
 * the methods or variables are even irrelevant.  However, subclasses
 * should be careful when not following the default path for computing
 * a tile.  Most importantly, when a subclass overrides
 * <code>getTile</code>, it should also override <code>computeTile</code>.
 *
 * <p> To request multiple tiles at a time, it is preferable to
 * call the <code>getTiles</code> method with a complete list of the
 * requested tiles' indices, than to call <code>getTile</code> once
 * per tile.  The implementation of <code>getTiles</code> in this class
 * is optimized using multi-threading so that multiple tiles are
 * computed simultaneously.
 *
 * @see PlanarImage
 * @see AreaOpImage
 * @see GeometricOpImage
 * @see PointOpImage
 * @see StatisticsOpImage
 * @see SourcelessOpImage
 *
 */
public abstract class OpImage extends PlanarImage {

    /**
     * A constant indicating that an operation is likely to
     * spend its time mainly performing computation.
     */
    public static final int OP_COMPUTE_BOUND = 1;

    /**
     * A constant indicating that an operation is likely to
     * spend its time mainly performing local I/O.
     */
    public static final int OP_IO_BOUND = 2;

    /**
     * A constant indicating that an operation is likely to
     * spend its time mainly performing network I/O.
     */
    public static final int OP_NETWORK_BOUND = 3;

    /**
     * A constant equal to what would be returned by
     * <code>ImageLayout.getValidMask()</code> if all fields were set.
     */
    private static final int LAYOUT_MASK_ALL =
        ImageLayout.MIN_X_MASK | ImageLayout.MIN_Y_MASK |
        ImageLayout.WIDTH_MASK | ImageLayout.HEIGHT_MASK |
        ImageLayout.TILE_GRID_X_OFFSET_MASK |
        ImageLayout.TILE_GRID_Y_OFFSET_MASK |
        ImageLayout.TILE_WIDTH_MASK | ImageLayout.TILE_HEIGHT_MASK |
        ImageLayout.SAMPLE_MODEL_MASK | ImageLayout.COLOR_MODEL_MASK;


    /**
     * The cache object used to cache this image's tiles.  It may refer
     * to a common cache shared by many <code>OpImage</code>s or a private
     * cache for this image only.  If it is <code>null</code>, it
     * indicates that this image does not have a tile cache.
     */
    protected transient TileCache cache;

    /**
     * Metric used to produce an ordered list of tiles.  This determines
     * which tiles are removed from the cache first if a memory control
     * operation is required.
     *
     * @since JAI 1.1
     */
    protected Object tileCacheMetric;

    /**
     * The scheduler to be used to schedule tile computation.
     */
    private transient TileScheduler scheduler =
        JAI.getDefaultInstance().getTileScheduler();

    /**
     * Variable indicating whether the TileScheduler is the Sun implementation.
     */
    private boolean isSunTileScheduler = false;

    /**
     * Indicates which one of the two <code>computeRect</code> variants
     * should be called by the <code>computeTile</code> method.  If it
     * is <code>true</code>, <code>computeRect</code> expects
     * contiguous sources.
     */
    protected boolean cobbleSources;

    /**
     * Whether dispose() has been invoked.
     */
    private boolean isDisposed = false;

    /**
     * Flag indicating that tile recycling is enabled for tiles which
     * may be referenced outside the API.
     */
    private boolean isCachedTileRecyclingEnabled = false;

    /**
     * A <code>TileRecycler</code> for use in <code>createTile()</code>.
     * May be <code>null</code>. This field is set by the configuration
     * map passed to {@link #OpImage(Vector,ImageLayout,Map,boolean}.
     *
     * @since JAI 1.1.2
     */
    protected TileRecycler tileRecycler;

    /** The default RasterAccessor format tags. */
    // XXX This variable should be removed if we stop using RasterAccessor.
    private RasterFormatTag[] formatTags = null;

    /**
     * Creates a new <code>ImageLayout</code> or forwards the argument
     * layout with or without modifications.
     *
     * <p> If the <code>layout</code> parameter is non-<code>null</code>
     * and all its fields are set then it is cloned and returned.
     *
     * <p> If the <code>layout</code> parameter is non-<code>null</code>
     * but some of its fields are not set and there is at least one source
     * available, then all fields of the layout which are not set are
     * copied from the corresponding attributes of the first source except
     * possibly the <code>ColorModel</code>.  The <code>ColorModel</code>
     * is copied if and only if the destination <code>SampleModel</code> is
     * non-<code>null</code> and the <code>ColorModel</code> and
     * <code>SampleModel</code> are compatible.
     *
     * The image's tile dimensions will be set by the first applicable 
     * means in the following priority-ordered list. Note that each tile
     * dimension, the <code>tileWidth</code> and the
     * <code>tileHeight</code>, is considered independently :
     * <ol>
     * <li>Tile dimension set in the <code>ImageLayout</code> (either by
     * the user or the operator itself);</li>
     * <li>Tile dimension of source, if source is non-<code>null</code>.
     * The tile dimension will be clamped to the minimum of that of the
     * source tile dimension and the image's corresponding dimension;</li>
     * <li>Non-<code>null</code> default tile size returned by 
     * <code>JAI.getDefaultTileSize()</code>, if the corresponding
     * image dimension is at least double the default tile size;</li>
     * <li>The dimensions of the image itself;</li>
     * </ol>
     *
     * <p> If the <code>layout</code> parameter is <code>null</code> and
     * there is at least one source available, a new layout is created from
     * the first source and returned directly.
     */
    private static ImageLayout layoutHelper(ImageLayout layout,
                                            Vector sources,
                                            Map config) {
        // Initialize to a reference to the layout passed in.
        ImageLayout il = layout;

        // Check the source Vector elements for nulls.
        if(sources != null) {
            checkSourceVector(sources, true);
        }

        // Check the class of the first source to avoid an exception here; a
        // class cast exception will be thrown by the PlanarImage constructor.
        RenderedImage im =
            sources != null && sources.size() > 0 &&
            sources.firstElement() instanceof RenderedImage ?
            (RenderedImage)sources.firstElement() : null;

        // Update some or all of the layout if a source is available.
        if(im != null) {
            // Create a new layout with the source as fallback.
            // The ColorModel field is intentionally NOT set.
            if(layout == null) {
                // Copy entirety of source image layout.
                il = layout = new ImageLayout(im);

                // Invalidate the ColorModel setting.
                il.unsetValid(ImageLayout.COLOR_MODEL_MASK);
            } else {
                // Set all fields except ColorModel.
                il = new ImageLayout(layout.getMinX(im),
                                     layout.getMinY(im),
                                     layout.getWidth(im),
                                     layout.getHeight(im),
                                     layout.getTileGridXOffset(im),
                                     layout.getTileGridYOffset(im),
                                     layout.getTileWidth(im),
                                     layout.getTileHeight(im),
                                     layout.getSampleModel(im),
                                     null);
            }

            // At this point "layout" and "il" are non-null with "layout"
            // representing the ImageLayout originally passed in.  "il" does
            // not yet have its ColorModel field set.

            // Set the ColorModel.
            if(layout.isValid(ImageLayout.COLOR_MODEL_MASK) &&
               layout.getColorModel(null) == null) {
                // User wants to force a null ColorModel.
                il.setColorModel(null);

            } else if(il.getSampleModel(null) != null) {

                // Target SampleModel is available.

                // Get SampleModel from "il"; guaranteed to be non-null.
                SampleModel sm = il.getSampleModel(null);

                // Get ColorModel from "layout".
                ColorModel cmLayout = layout.getColorModel(null);

                // First attempt to set the ColorModel to that specified
                // in the original layout, if any.
                if(cmLayout != null) {
                    // ColorModel is set in original layout.
                    if(JDKWorkarounds.areCompatibleDataModels(sm, cmLayout)) {
                        // ColorModel is compatible with target SampleModel.
                        il.setColorModel(cmLayout);

                    } else if(layout.getSampleModel(null) == null) {
                        // SampleModel not set in original layout so
                        // ColorModel must be incompatible with source
                        // SampleModel: create a compatible SampleModel.

                        // Set the ColorModel to that desired.
                        il.setColorModel(cmLayout);

                        // Derive a new SampleModel from the desired ColorModel
                        SampleModel derivedSM =
                            cmLayout.createCompatibleSampleModel(
                                il.getTileWidth(null),
                                il.getTileHeight(null));

                        // Set the SampleModel to that derived from the CM.
                        il.setSampleModel(derivedSM);

                    }
                }

                // If ColorModel not set from ImageLayout, attempt to set
                // using a ColorModelFactory and if that fails, attempt to
                // use the ColorModel of the source.
                if(!il.isValid(ImageLayout.COLOR_MODEL_MASK) &&
                   !setColorModelFromFactory(sm, sources, config, il)) {
                    // Get ColorModel from "im", i.e., the source.
                    ColorModel cmSource = im.getColorModel();
                    if(cmSource != null &&
                       JDKWorkarounds.areCompatibleDataModels(sm, cmSource)) {
                        // Set to source ColorModel.
			if (cmSource != null &&
			    cmSource instanceof IndexColorModel &&
			    config != null &&
			    config.containsKey(
					 JAI.KEY_REPLACE_INDEX_COLOR_MODEL) &&
			    ((Boolean)config.get(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)).booleanValue()) {
			    
			    ColorModel newCM = 
				PlanarImage.getDefaultColorModel(
						  sm.getDataType(),
						  cmSource.getNumComponents());

			    SampleModel newSM;
			    if (newCM != null) {
				newSM = 
				    newCM.createCompatibleSampleModel(
						       il.getTileWidth(null),
						       il.getTileHeight(null));
			    } else {
				newSM = 
			      RasterFactory.createPixelInterleavedSampleModel(
						  sm.getDataType(),
						  il.getTileWidth(null),
						  il.getTileHeight(null),
						  cmSource.getNumComponents());
			    }

			    il.setSampleModel(newSM);			    
			    if (newCM != null) 
				il.setColorModel(newCM);
			} else {
			    il.setColorModel(cmSource);
			}
                    }
                }
            } else if(il.getSampleModel(null) == null) { // null SampleModel
                // Set to whatever is available.
                il.setColorModel(layout.getColorModel(im));
            }
            // end of block if(im != null)
        } else if(il != null) {
            // Can only get here if im == null && layout != null.
            // Make sure that il is a clone of layout.
            il = (ImageLayout)layout.clone();

            // If the ColorModel is set but the SampleModel is not,
            // derive a SampleModel from the ColorModel.
            if(il.getColorModel(null) != null &&
               il.getSampleModel(null) == null) {
                // Set SampleModel dimensions.
                int smWidth = il.getTileWidth(null);
                if(smWidth == 0) {
                    smWidth = 512;
                }
                int smHeight = il.getTileHeight(null);
                if(smHeight == 0) {
                    smHeight = 512;
                }

                // Derive a new SampleModel from the desired ColorModel
                SampleModel derivedSM =
                    il.getColorModel(null).createCompatibleSampleModel(smWidth,
                                                                       smHeight);

                // Set the SampleModel to that derived from the CM.
                il.setSampleModel(derivedSM);
            }
        } // end of block if(il != null)

        // If no ColorModel is set, then first attempt to set a ColorModel
        // using the ColorModelFactory; otherwise set a default ColorModel
        // if either the configuration mapping is null, it does not contain
        // a mapping of the key KEY_DEFAULT_COLOR_MODEL_ENABLED, or this key
        // is mapped to Boolean.TRUE.
        if(il != null &&
           !il.isValid(ImageLayout.COLOR_MODEL_MASK) &&
           il.getSampleModel(null) != null &&
           !setColorModelFromFactory(il.getSampleModel(null),
                                     sources, config, il)) {

	    ColorModel cm = null;
	    SampleModel srcSM = il.getSampleModel(null);
	    
	    // If it is not a byte image, then probably all the above did not
	    // manage to set a ColorModel and ImageUtil.getCompatibleColorModel
	    // will be used to get the ColorModel. However we want to ensure
	    // that the destination ColorModel is expanded if the ColorModel
	    // of the source is an IndexColorModel and we've been asked to
	    // do IndexColorModel expansion
	    if (im != null &&
		im.getColorModel() != null && 
		im.getColorModel() instanceof IndexColorModel &&
		config != null &&
		config.containsKey(JAI.KEY_REPLACE_INDEX_COLOR_MODEL) &&
		((Boolean)config.get(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)).booleanValue()) {

		IndexColorModel icm = (IndexColorModel)im.getColorModel();

		// We need to change the ColorModel to a non IndexColorModel
		// so that operations such as geometric can take place
		// correctly. If the ColorModel is changed the SampleModel
		// needs to be changed too, so that they are compatible
		cm = PlanarImage.getDefaultColorModel(srcSM.getDataType(),
						      icm.getNumComponents());
		
		SampleModel newSM;
		if (cm != null) {
		    newSM = cm.createCompatibleSampleModel(srcSM.getWidth(),
							   srcSM.getHeight());
		} else {
		    newSM = RasterFactory.createPixelInterleavedSampleModel(
						      srcSM.getDataType(),
						      srcSM.getWidth(),
						      srcSM.getHeight(),
						      icm.getNumComponents());
		}

		il.setSampleModel(newSM);

	    } else {

		cm = ImageUtil.getCompatibleColorModel(il.getSampleModel(null),
						       config);
	    }

            // Set ColorModel only if method succeeded.
            if(cm != null)
		il.setColorModel(cm);
        }

        // If the tile dimensions were not set in "layout" and are either
        // not set in "il" or would yield an untiled image then reset them to
        // the global tile size default if each image dimension is at least
        // double the respective default tile dimension.
        if(layout != null && il != null &&
           !layout.isValid(ImageLayout.TILE_WIDTH_MASK|
                           ImageLayout.TILE_HEIGHT_MASK)) {
            Dimension defaultTileSize = JAI.getDefaultTileSize();
            if(defaultTileSize != null) {
                if(!layout.isValid(ImageLayout.TILE_WIDTH_MASK)) {
                    if(il.getTileWidth(null) <= 0) {
                        il.setTileWidth(defaultTileSize.width);
                    } else {
                        // Calculate number of tiles along X.
                        int numX =
                            XToTileX(il.getMinX(null) + il.getWidth(null) - 1,
                                     il.getTileGridXOffset(null),
                                     il.getTileWidth(null)) -
                            XToTileX(il.getMinX(null),
                                     il.getTileGridXOffset(null),
                                     il.getTileWidth(null)) + 1;
                    
                        // Reset if single-tiled and image is big enough in X.
                        if(numX <= 1
                           && il.getWidth(null) >= 2*defaultTileSize.width) {
                            il.setTileWidth(defaultTileSize.width);
                        }
                    }
                }

                if(!layout.isValid(ImageLayout.TILE_HEIGHT_MASK)) {
                    if(il.getTileHeight(null) <= 0) {
                        il.setTileHeight(defaultTileSize.height);
                    } else {
                        // Calculate number of tiles along Y.
                        int numY =
                            YToTileY(il.getMinY(null) + il.getHeight(null) - 1,
                                     il.getTileGridYOffset(null),
                                     il.getTileHeight(null)) -
                            YToTileY(il.getMinY(null),
                                     il.getTileGridYOffset(null),
                                     il.getTileHeight(null)) + 1;

                        // Reset if single-tiled and image is big enough in Y.
                        if(numY <= 1
                           && il.getHeight(null) >= 2*defaultTileSize.height) {
                            il.setTileHeight(defaultTileSize.height);
                        }
                    }
                }
            }
        }

	// Independently clamp each tile dimension to the respective image
	// dimension, if the tile dimensions are not set in any supplied
	// ImageLayout, and the tile and image dimensions are both set in
	// the ImageLayout to be returned.

	// Tile width
	if ((layout == null ||
	     !layout.isValid(ImageLayout.TILE_WIDTH_MASK)) &&
	    il.isValid(ImageLayout.TILE_WIDTH_MASK | 
		       ImageLayout.WIDTH_MASK) &&
	    il.getTileWidth(null) > il.getWidth(null)) {
	    il.setTileWidth(il.getWidth(null));
	}
	
	// Tile height
	if ((layout == null || 
	     !layout.isValid(ImageLayout.TILE_HEIGHT_MASK)) &&
	    il.isValid(ImageLayout.TILE_HEIGHT_MASK | 
		       ImageLayout.HEIGHT_MASK) &&
	    il.getTileHeight(null) > il.getHeight(null)) {
	    il.setTileHeight(il.getHeight(null));
	}

        return il;
    }

    /**
     * Set the <code>ColorModel</code> in <code>layout</code> if
     * <code>config</code> is non-<code>null</code> and contains a mapping
     * for <code>JAI.KEY_COLOR_MODEL_FACTORY</code>.  If the
     * <code>ColorModelFactory</code> returns a non-<code>null</code>
     * <code>ColorModel</code> which is compatible with
     * <code>sampleModel</code> it is used to set the <code>ColorModel</code>
     * in <code>layout</code>.
     *
     * @param sampleModel The <code>SampleModel</code> to which the
     *        <code>ColorModel</code> to be created must correspond;
     *        may <b>not</b> be <code>null</code>.
     * @param sources A <code>List</code> of <code>RenderedImage</code>s;
     *        may be <code>null</code>.
     * @param config A configuration mapping; may be
     *        <code>null</code>.
     * @param layout The image layout; will be updated with the
     *        <code>ColorModel</code>created by the
     *        <code>ColorModelFactory</code>
     *
     * @return Whether the <code>ColorModel</code> in <code>layout</code>
     *         has been set.
     * @exception IllegalArgumentException if <code>sampleModel</code>
     *            is <code>null</code>.
     */
    private static boolean setColorModelFromFactory(SampleModel sampleModel,
                                                    Vector sources,
                                                    Map config,
                                                    ImageLayout layout) {
        boolean isColorModelSet = false;

        if(config != null &&
           config.containsKey(JAI.KEY_COLOR_MODEL_FACTORY)) {
            ColorModelFactory cmf =
                (ColorModelFactory)config.get(JAI.KEY_COLOR_MODEL_FACTORY);
            ColorModel cm = cmf.createColorModel(sampleModel,
                                                 sources,
                                                 config);
            if(cm != null &&
               JDKWorkarounds.areCompatibleDataModels(sampleModel, cm)) {
                layout.setColorModel(cm);
                isColorModelSet = true;
            }
        }

        return isColorModelSet;
    }

    // XXX Note: ColorModel.isCompatibleRaster() is mentioned below but it
    // should be ColorModel.isCompatibleSampleModel(). This has a bug
    // however (4326636) which is worked around within JAI. The other method
    // is mentioned as it does NOT have a bug.
    /**
     * Constructor.
     *
     * <p> The image's layout is encapsulated in the <code>layout</code>
     * argument.  The variables of the image layout which are not set in
     * the <code>layout</code> parameter are copied from the first source
     * if sources are available.  In the case of the <code>ColorModel</code>,
     * the copy is performed if and only if the <code>ColorModel</code> is
     * compatible with the destination <code>SampleModel</code> and is not
     * set by another higher priority mechanism as described presently.</p>
     *
     * <p> Assuming that there is at least one source, the image's
     * <code>ColorModel</code> will be set by the first applicable means
     * in the following priority-ordered list:
     * <ol>
     * <li><code>null</code> <code>ColorModel</code> from
     * <code>ImageLayout</code>;</li>
     * <li>Non-<code>null</code> <code>ColorModel</code> from
     * <code>ImageLayout</code> if compatible with
     * <code>SampleModel</code> in <code>ImageLayout</code> or if
     * <code>SampleModel</code> in <code>ImageLayout</code> is
     * <code>null</code>;</li>
     * <li>Value returned by <code>ColorModelFactory</code> set via the
     * <code>JAI.KEY_COLOR_MODEL_FACTORY</code> configuration variable if
     * compatible with <code>SampleModel</code>;</li>
     * <li>An instance of a non-<code>IndexColorModel</code> (or
     * <code>null</code> if no compatible non-<code>IndexColorModel</code> 
     * could be generated), if the source has an <code>IndexColorModel</code> 
     * and <code>JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code>
     * is <code>Boolean.TRUE</code>;</li>
     * <li><code>ColorModel</code> of first source if compatible with
     * <code>SampleModel</code>;</li>
     * <li>Value returned by default method specified by the
     * <code>JAI.KEY_DEFAULT_COLOR_MODEL_METHOD</code> configuration variable
     * if <code>JAI.KEY_DEFAULT_COLOR_MODEL_ENABLED</code> is
     * <code>Boolean.TRUE</code>.</li>
     * </ol>
     * If it is not possible to set the <code>ColorModel</code> by any of
     * these means it will remain <code>null</code>.</p>
     *
     * The image's tile dimensions will be set by the first applicable 
     * means in the following priority-ordered list. Note that each tile
     * dimension, the <code>tileWidth</code> and the
     * <code>tileHeight</code>, is considered independently :
     * <ol>
     * <li>Tile dimension set in the <code>ImageLayout</code> (either by
     * the user or the operator itself);</li>
     * <li>Tile dimension of source, if source is non-<code>null</code>.
     * The tile dimension will be clamped to the minimum of that of the
     * source tile dimension and the image's corresponding dimension;</li>
     * <li>Non-<code>null</code> default tile size returned by 
     * <code>JAI.getDefaultTileSize()</code>, if the corresponding
     * image dimension is at least double the default tile size;</li>
     * <li>The dimensions of the image itself;</li>
     * </ol>
     *
     * <p> The <code>sources</code> contains a list of immediate sources
     * of this image.  Elements in the list may not be <code>null</code>.
     * If this image has no sources this argument should be <code>null</code>.
     * This parameter is forwarded unmodified to the <code>PlanarImage</code>
     * constructor.
     *
     * <p> The <code>configuration</code> contains a mapping of configuration
     * variables and image properties.  Entries which have keys of type
     * <code>RenderingHints.Key</code> are taken to be configuration variables.
     * Entries with a key which is either a <code>String</code> or a
     * <code>CaselessStringKey</code> are interpreted as image properties.
     * This parameter is forwarded unmodified to the <code>PlanarImage</code>
     * constructor.
     *
     * <p> This image class recognizes the configuration variables referenced
     * by the following keys:
     *
     * <ul>
     * <li> <code>JAI.KEY_TILE_CACHE</code>: specifies the
     * <code>TileCache</code> in which to store the image tiles;
     * if this key is not supplied no tile caching will be performed.
     * <li> <code>JAI.KEY_TILE_CACHE_METRIC</code>: establishes an
     * ordering of tiles stored in the tile cache.  This ordering
     * is used to determine which tiles will be removed first, if
     * a condition causes tiles to be removed from the cache.
     * <li> <code>JAI.KEY_TILE_SCHEDULER</code>: specifies the
     * <code>TileScheduler</code> to use to schedule tile computation;
     * if this key is not supplied the default scheduler will be used.
     * <li> <code>JAI.KEY_COLOR_MODEL_FACTORY</code>: specifies a
     * <code>ColorModelFactory</code> to be used to generate the
     * <code>ColorModel</code> of the image.  If such a callback is
     * provided it will be invoked if and only if either no
     * <code>ImageLayout</code> hint is given, or an <code>ImageLayout</code>
     * hint is given but contains a non-<code>null</code>
     * <code>ColorModel</code> which is incompatible with the image
     * <code>SampleModel</code>.  In other words, such a callback provides
     * the second priority mechanism for setting the <code>ColorModel</code>
     * of the image.</li>
     * <li> <code>JAI.KEY_DEFAULT_COLOR_MODEL_ENABLED</code>: specifies whether
     * a default <code>ColorModel</code> will be derived
     * if none is specified and one cannot be inherited from the first source;
     * if this key is not supplied a default <code>ColorModel</code> will be
     * computed if necessary.
     * <li> <code>JAI.KEY_DEFAULT_COLOR_MODEL_METHOD</code>: specifies the
     * method to be used to compute the default <code>ColorModel</code>;
     * if this key is not supplied and a default <code>ColorModel</code> is
     * required, <code>PlanarImage.createColorModel()</code> will be used to
     * compute it.
     * <li> <code>JAI.KEY_TILE_FACTORY</code>: specifies a
     * {@link TileFactory} to be used to generate the tiles of the
     * image via {@link TileFactory#createTile(SampleModel,Point)}.  If
     * no such configuration variable is given, a new <code>Raster</code>
     * will be created for each image tile.  This behavior may be
     * overridden by subclasses which have alternate means of saving
     * memory, for example as in the case of point operations which
     * may overwrite a source image not referenced by user code. Note
     * that the corresponding instance variable is actually set by
     * the superclass constructor.</li>
     * <li> <code>JAI.KEY_TILE_RECYCLER</code>: specifies a
     * {@link TileRecycler} to be used to recycle the tiles of the
     * image when the <code>dispose()</code> method is invoked.  If
     * such a configuration variable is set, the image has a
     * non-<code>null</code> <code>TileCache</code>, and tile recycling
     * is enabled, then invoking <code>dispose()</code> will cause each
     * of the tiles of this image currently in the cache to be passed to
     * the configured <code>TileRecycler</code></li> via
     * {@link TileRecycler#recycleTile(Raster)}.</li>
     * <li> <code>JAI.KEY_CACHED_TILE_RECYCLING_ENABLED</code>: specifies a
     * <code>Boolean</code> value which indicates whether {#dispose()}
     * should pass to <code>tileRecycler.recycleTile()</code> any image
     * tiles remaining in the cache.</li>
     * </ul>
     *
     * <p> The <code>cobbleSources</code> indicates which one of the two
     * variants of the <code>computeRect</code> method should be called.
     * If a subclass does not follow the default tile computation scheme,
     * then this argument may be irrelevant.
     *
     * @param layout  The layout of this image.
     * @param sources  The immediate sources of this image.
     * @param configuration Configurable attributes of the image including
     *        configuration variables indexed by
     *        <code>RenderingHints.Key</code>s and image properties indexed
     *        by <code>String</code>s or <code>CaselessStringKey</code>s.
     *        This parameter may be <code>null</code>.
     * @param cobbleSources  Indicates which variant of the
     *        <code>computeRect</code> method should be called.
     *
     * @throws IllegalArgumentException  If <code>sources</code>
     *         is non-<code>null</code> and any object in
     *         <code>sources</code> is <code>null</code>.
     * @throws RuntimeException If default <code>ColorModel</code> setting
     *         is enabled via a hint in the configuration <code>Map</code>
     *         and the supplied <code>Method</code> does not conform to the
     *         requirements stated in the <code>JAI</code> class for the
     *         hint key <code>KEY_DEFAULT_COLOR_MODEL_METHOD</code>.
     *
     * @since JAI 1.1
     */
    public OpImage(Vector sources,
                   ImageLayout layout,
                   Map configuration,
                   boolean cobbleSources) {
        super(layoutHelper(layout, sources, configuration),
              sources, configuration);

        if(configuration != null) {
            // Get the cache from the configuration map.
            Object cacheConfig = configuration.get(JAI.KEY_TILE_CACHE);

            // Ensure that it is a TileCache instance with positive capacity.
            if(cacheConfig != null &&
               cacheConfig instanceof TileCache &&
               ((TileCache)cacheConfig).getMemoryCapacity() > 0) {
                cache = (TileCache)cacheConfig;
            }

            // Get the scheduler from the configuration map.
            Object schedulerConfig = configuration.get(JAI.KEY_TILE_SCHEDULER);

            // Ensure that it is a TileScheduler instance.
            if(schedulerConfig != null &&
               schedulerConfig instanceof TileScheduler) {
                scheduler = (TileScheduler)schedulerConfig;
            }

            try {
                // Test whether the TileScheduler is the default type.
                Class sunScheduler =
                    Class.forName("com.lightcrafts.media.jai.util.SunTileScheduler");

                isSunTileScheduler = sunScheduler.isInstance(scheduler);
            } catch(Exception e) {
                // Deliberately ignore any Exceptions.
            }

            // Get the tile metric (cost or priority, for example)
            tileCacheMetric = configuration.get(JAI.KEY_TILE_CACHE_METRIC);

            // Set up cached tile recycling flag.
            Object recyclingEnabledValue =
                configuration.get(JAI.KEY_CACHED_TILE_RECYCLING_ENABLED);
            if(recyclingEnabledValue instanceof Boolean) {
                isCachedTileRecyclingEnabled =
                    ((Boolean)recyclingEnabledValue).booleanValue();
            }

            // Set up the TileRecycler.
            Object recyclerValue = configuration.get(JAI.KEY_TILE_RECYCLER);
            if(recyclerValue instanceof TileRecycler) {
                tileRecycler = (TileRecycler)recyclerValue;
            }
        }

        this.cobbleSources = cobbleSources;
    }

    /**
     * A <code>TileComputationListener</code> to pass to the
     * <code>TileScheduler</code> to intercept method calls such that the
     * computed tiles are added to the <code>TileCache</code> of the image.
     */
    private class TCL implements TileComputationListener {
        OpImage opImage;

        private TCL(OpImage opImage) {
            this.opImage = opImage;
        }

        public void tileComputed(Object eventSource,
                                 TileRequest[] requests,
                                 PlanarImage image, int tileX, int tileY,
                                 Raster tile) {
            if(image == opImage) {
                // Cache the tile.
                addTileToCache(tileX, tileY, tile);
            }
        }

        public void tileCancelled(Object eventSource,
                                  TileRequest[] requests,
                                  PlanarImage image, int tileX, int tileY) {
            // Do nothing.
        }

        public void tileComputationFailure(Object eventSource,
                                           TileRequest[] requests,
                                           PlanarImage image,
                                           int tileX, int tileY,
                                           Throwable situation) {
            // Do nothing.
        }
    }

    /**
     * Stores a <code>RenderedImage</code> in a <code>Vector</code>.
     *
     * @param image The image to be stored in the <code>Vector</code>.
     *
     * @return A <code>Vector</code> containing the image.
     *
     * @throws IllegalArgumentException if <code>image</code> is
     * <code>null</code>.
     *
     * @since JAI 1.1
     */
    protected static Vector vectorize(RenderedImage image) {
        if(image == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage3"));
        }
        Vector v = new Vector(1);
        v.addElement(image);
        return v;
    }

    /**
     * Stores two <code>RenderedImage</code>s in a <code>Vector</code>.
     *
     * @param image1 The first image to be stored in the <code>Vector</code>.
     * @param image2 The second image to be stored in the <code>Vector</code>.
     *
     * @return A <code>Vector</code> containing the images.
     *
     * @throws IllegalArgumentException if <code>image1</code> or
     * <code>image2</code> is <code>null</code>.
     *
     * @since JAI 1.1
     */
    protected static Vector vectorize(RenderedImage image1,
                                      RenderedImage image2) {
        if(image1 == null || image2 == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage3"));
        }
        Vector v = new Vector(2);
        v.addElement(image1);
        v.addElement(image2);
        return v;
    }

    /**
     * Stores three <code>RenderedImage</code>s in a <code>Vector</code>.
     *
     * @param image1 The first image to be stored in the <code>Vector</code>.
     * @param image2 The second image to be stored in the <code>Vector</code>.
     * @param image3 The third image to be stored in the <code>Vector</code>.
     *
     * @return A <code>Vector</code> containing the images.
     *
     * @throws IllegalArgumentException if <code>image1</code> or
     * <code>image2</code> or <code>image3</code> is <code>null</code>.
     *
     * @since JAI 1.1
     */
    protected static Vector vectorize(RenderedImage image1,
                                      RenderedImage image2,
                                      RenderedImage image3) {
        if(image1 == null || image2 == null || image3 == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage3"));
        }
        Vector v = new Vector(3);
        v.addElement(image1);
        v.addElement(image2);
        v.addElement(image3);
        return v;
    }

    /**
     * Checks the source <code>Vector</code>.
     *
     * <p>Checks whether the <code>sources</code> parameter is <code>null</code>
     * and optionally whether all elements are non-<code>null</code>.
     *
     * @param sources The source <code>Vector</code>.
     * @param checkElements Whether the elements are to be checked.
     *
     * @return The <code>sources</code> parameter unmodified.
     *
     * @throws IllegalArgumentException  If <code>sources</code>
     *         is <code>null</code>.
     * @throws IllegalArgumentException  If <code>checkElements</code>
     *         is <code>true</code>, <code>sources</code>
     *         is non-<code>null</code> and any object in
     *         <code>sources</code> is <code>null</code>.
     */
    static Vector checkSourceVector(Vector sources, boolean checkElements) {
        // Check for null source Vector.
        if(sources == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage2"));
        }

        if(checkElements) {
            // Check Vector elements.
            int numSources = sources.size();
            for(int i = 0; i < numSources; i++) {
                // Check for null element.
                if(sources.get(i) == null) {
                    throw new
                        IllegalArgumentException(JaiI18N.getString("OpImage3"));
                }
            }
        }
        
        return sources;
    }

    /**
     * Returns the tile cache object of this image by reference.
     * If this image does not have a tile cache, this method returns
     * <code>null</code>.
     *
     * @since JAI 1.1
     */
    public TileCache getTileCache() {
        return cache;
    }

    /**
     * Sets the tile cache object of this image.  A <code>null</code>
     * input indicates that this image should have no tile cache and
     * subsequently computed tiles will not be cached.
     *
     * <p> The existing cache object is informed to release all the
     * currently cached tiles of this image.
     *
     * @param cache  A cache object to be used for caching this image's
     *        tiles, or <code>null</code> if no tile caching is desired.
     */
    public void setTileCache(TileCache cache) {
        if (this.cache != null) {
            this.cache.removeTiles(this);
        }
        this.cache = cache;
    }
    
    /**
     * Retrieves a tile from the tile cache.  If this image does not
     * have a tile cache, or the requested tile is not currently in
     * the cache, this method returns <code>null</code>.
     *
     * @param tileX  The X index of the tile.
     * @param tileY  The Y index of the tile.
     *
     * @return The requested tile as a <code>Raster</code> or
     *         <code>null</code>.
     */
    protected Raster getTileFromCache(int tileX, int tileY) {
        return cache != null ? cache.getTile(this, tileX, tileY) : null;
    }
    
    /**
     * Adds a tile to the tile cache.  If this image does not have
     * a tile cache, this method does nothing.
     *
     * @param tileX  The X index of the tile.
     * @param tileY  The Y index of the tile.
     * @param tile  The tile to be added to the cache.
     */
    protected void addTileToCache(int tileX,
                                  int tileY,
                                  Raster tile) {
        if (cache != null) {
            cache.add(this, tileX, tileY, tile, tileCacheMetric);
        }
    }

    /**
     * Returns the <code>tileCacheMetric</code> instance variable by reference.
     *
     * @since JAI 1.1
     */
    public Object getTileCacheMetric() {
        return tileCacheMetric;
    }

    /**
     * Returns a tile of this image as a <code>Raster</code>.  If the
     * requested tile is completely outside of this image's bounds,
     * this method returns <code>null</code>.
     *
     * <p> This method attempts to retrieve the requested tile from the
     * cache.  If the tile is not currently in the cache, it schedules
     * the tile for computation and adds it to the cache once the tile
     * has been computed.
     *
     * <p> If a subclass overrides this method, then it needs to handle
     * tile caching and scheduling.  It should also override
     * <code>computeTile()</code> which may be invoked directly by the
     * <code>TileScheduler</code>.
     *
     * @param tileX  The X index of the tile.
     * @param tileY  The Y index of the tile.
     */
    public Raster getTile(int tileX, int tileY) {
        Raster tile = null;	// the requested tile, to be returned

        // Make sure the requested tile is inside this image's boundary.
        if (tileX >= getMinTileX() && tileX <= getMaxTileX() &&
            tileY >= getMinTileY() && tileY <= getMaxTileY()) {
            // Check if tile is available in the cache.
            tile = getTileFromCache(tileX, tileY);

            if (tile == null) {         // tile not in cache
                try {
                    tile = scheduler.scheduleTile(this, tileX, tileY);
                } catch (OutOfMemoryError e) {
                    // Empty the cache and call System.gc()
                    if(cache != null) {
                        cache.flush();
                        System.gc(); //slow
                    }

                    // Need to reissue the tile scheduling.
                    tile = scheduler.scheduleTile(this, tileX, tileY);
                }

                // Cache the result tile.
                addTileToCache(tileX, tileY, tile);
            }
        }

        return tile;
    }

    /**
     * Computes the image data of a tile.
     *
     * <p> When a tile is requested via the <code>getTile</code> method
     * and that tile is not in this image's tile cache, this method is
     * invoked by the <code>TileScheduler</code> to compute the data of
     * the new tile.  Even though this method is marked <code>public</code>,
     * it should not be called by the applications directly.  Rather, it
     * is meant to be called by the <code>TileScheduler</code> for the
     * actual computation.
     *
     * <p> The implementation of this method in this class assumes that
     * the requested tile either intersects the image, or is within the
     * image's bounds.  It creates a new <code>Raster</code> to
     * represent the requested tile, then calls one of the two variants
     * of <code>computeRect</code> to calculate the pixels of the
     * tile that are within the image's bounds.  The value of
     * <code>cobbleSources</code> determines which variant of
     * <code>computeRect</code> is invoked, as described in the class
     * comments.
     *
     * <p> Subclasses may provide a more optimized implementation of this
     * method.  If they override this method not to call either variant of
     * <code>computeRect</code>, then neither variant of
     * <code>computeRect</code> needs to be implemented.
     *
     * @param tileX  The X index of the tile.
     * @param tileY  The Y index of the tile.
     */
    public Raster computeTile(int tileX, int tileY) {
        // Create a new Raster.
        WritableRaster dest = createWritableRaster(sampleModel,
                                                   new Point(tileXToX(tileX),
                                                             tileYToY(tileY)));
            
        // Determine the active area; tile intersects with image's bounds.
        Rectangle destRect = getTileRect(tileX, tileY);

        int numSources = getNumSources();

        if (cobbleSources) {
            Raster[] rasterSources = new Raster[numSources];
            // Cobble areas
            for (int i = 0; i < numSources; i++) {
                PlanarImage source = getSource(i);
                Rectangle srcRect = mapDestRect(destRect, i);

                // If srcRect is empty, set the Raster for this source to
                // null; otherwise pass srcRect to getData(). If srcRect
                // is null, getData() will return a Raster containing the
                // data of the entire source image.
                rasterSources[i] = srcRect != null && srcRect.isEmpty() ?
                    null : source.getData(srcRect);
            }
            computeRect(rasterSources, dest, destRect);

            for (int i = 0; i < numSources; i++) {
                Raster sourceData = rasterSources[i];
                if(sourceData != null) {
                    PlanarImage source = getSourceImage(i);

                    // Recycle the source tile
                    if(source.overlapsMultipleTiles(sourceData.getBounds())) {
                        recycleTile(sourceData);
                    }
                }
            }
        } else {
            PlanarImage[] imageSources = new PlanarImage[numSources];
            for (int i = 0; i < numSources; i++) {
                imageSources[i] = getSource(i);
            }
            computeRect(imageSources, dest, destRect);
        }

        return dest;
    }

    /**
     * Computes a rectangle of output, given <code>Raster</code>
     * sources.  This method should be overridden by
     * <code>OpImage</code> subclasses that make use of cobbled
     * sources, as determined by the setting of the
     * <code>cobbleSources</code> constructor argument to this class.
     *
     * <p> The source <code>Raster</code>s are guaranteed to include
     * at least the area specified by <code>mapDestRect(destRect)</code>
     * unless this area is empty or does not intersect the corresponding
     * source in which case the source <code>Raster</code>
     * will be <code>null</code>.  Only the specified destination region
     * should be written.</p>
     *
     * <p> Since the subclasses of <code>OpImage</code> may choose
     * between the cobbling and non-cobbling versions of
     * <code>computeRect</code>, it is not possible to leave this
     * method abstract in <code>OpImage</code>.  Instead, a default
     * implementation is provided that throws a
     * <code>RuntimeException</code>.</p>
     *
     * @param sources an array of source <code>Raster</code>s, one per
     *        source image.
     * @param dest a <code>WritableRaster</code> to be filled in.
     * @param destRect the <code>Rectangle</code> within the
     *        destination to be written.
     *
     * @throws RuntimeException  If this method is invoked on the subclass
     *         that sets <code>cobbleSources</code> to <code>true</code>
     *         but does not supply an implementation of this method.
     */
    protected void computeRect(Raster[] sources,
                               WritableRaster dest,
                               Rectangle destRect) {
        String className = this.getClass().getName();
        throw new RuntimeException(className + " " +
                                   JaiI18N.getString("OpImage0"));
    }

    /**
     * Computes a rectangle of output, given <code>PlanarImage</code>
     * sources.  This method should be overridden by
     * <code>OpImage</code> subclasses that do not require cobbled
     * sources; typically they will instantiate iterators to perform
     * source access, but they may access sources directly (via the
     * <code>SampleModel</code>/<code>DataBuffer</code> interfaces) if
     * they wish.
     *
     * <p> Since the subclasses of <code>OpImage</code> may choose
     * between the cobbling and non-cobbling versions of
     * <code>computeRect</code>, it is not possible to leave this
     * method abstract in <code>OpImage</code>.  Instead, a default
     * implementation is provided that throws a
     * <code>RuntimeException</code>.
     *
     * @param sources an array of <code>PlanarImage</code> sources.
     * @param dest a <code>WritableRaster</code> to be filled in.
     * @param destRect the <code>Rectangle</code> within the
     * destination to be written.
     *
     * @throws RuntimeException  If this method is invoked on the subclass
     *         that sets <code>cobbleSources</code> to <code>false</code>
     *         but does not supply an implementation of this method.
     */
    protected void computeRect(PlanarImage[] sources,
                               WritableRaster dest,
                               Rectangle destRect) {
        String className = this.getClass().getName();
        throw new RuntimeException(className + " " +
                                   JaiI18N.getString("OpImage1"));
    }

    /**
     * Returns a list of indices of the tiles of a given source image
     * that may be required in order to compute a given tile.
     * Ideally, only tiles that will be requested by means of calls to
     * the source's <code>getTile()</code> method should be reported.
     * The default implementation uses <code>mapDestRect()</code> to
     * obtain a conservative estimate.
     *
     * <p> If no dependencies exist, this method returns
     * <code>null</code>.
     *
     * <p> This method may be used by optimized implementations of JAI
     * in order to predict future work and create an optimized
     * schedule for performing it.
     *
     * <p> A given <code>OpImage</code> may mix calls to
     * <code>getTile()</code> with calls to other methods such as
     * <code>getData()</code> and <code>copyData()</code> in order to
     * avoid requesting entire tiles where only a small portion is
     * needed.  In such a case, this method may be overridden to
     * provide a more accurate estimate of the set of
     * <code>getTile()</code> calls that will actually be performed.
     *
     * @param tileX the X index of the tile.
     * @param tileY the Y index of the tile.
     * @param sourceIndex the index of the source image.
     *
     * @return An array of <code>Point</code>s indicating the source
     *         tile dependencies.
     *
     * @throws IllegalArgumentException  If <code>sourceIndex</code> is
     *         negative or greater than the index of the last source.
     */
    public Point[] getTileDependencies(int tileX, int tileY,
                                       int sourceIndex) {
        if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            // Specified source does not exist for this image.
            throw new IllegalArgumentException(
                JaiI18N.getString("Generic1"));
        }

        Rectangle rect = getTileRect(tileX, tileY);
        if (rect.isEmpty()) {
            // The tile is outside of the image bounds.
            return null;
        }

        // Returns a list of tiles that belong to the source specified by
        // the <code>sourceIndex</code> argument, which are need to compute
        // the pixels within the rectangle region specified by the
        // <code>rect</code> argument of this image.
        //
        // This method uses <code>mapDestRect</code> to conservatively
        // determine the source region required.  However, only those tiles
        // actually inside the source image bound are returned.  If the
        // region of interest maps completely outside of the source image,
        // <code>null</code> is returned.
        PlanarImage src = getSource(sourceIndex);
        Rectangle srcRect = mapDestRect(rect, sourceIndex);

        int minTileX = src.XToTileX(srcRect.x);
        int maxTileX = src.XToTileX(srcRect.x + srcRect.width - 1);

        int minTileY = src.YToTileY(srcRect.y);
        int maxTileY = src.YToTileY(srcRect.y + srcRect.height - 1);

        // Make sure the tiles are really inside the source image.
        minTileX = Math.max(minTileX, src.getMinTileX());
        maxTileX = Math.min(maxTileX, src.getMaxTileX());

        minTileY = Math.max(minTileY, src.getMinTileY());
        maxTileY = Math.min(maxTileY, src.getMaxTileY());

        int numXTiles = maxTileX - minTileX + 1;
        int numYTiles = maxTileY - minTileY + 1;
        if (numXTiles <= 0 || numYTiles <= 0) {
            // The tile maps outside of source image bound.
            return null;
        }

        Point[] ret = new Point[numYTiles*numXTiles];
        int i = 0;

        for (int y = minTileY; y <= maxTileY; y++) {
            for (int x = minTileX; x <= maxTileX; x++) {
                ret[i++] = new Point(x, y);
            }
        }

        return ret;
    }

    /**
     * Computes the tiles indicated by the given tile indices.  This
     * call is preferable to a series of <code>getTile()</code> calls
     * because certain implementations can make optimizations based on
     * the knowledge that multiple tiles are being asked for at once.
     *
     * <p> The implementation of this method in this class uses multiple
     * threads to compute multiple tiles at a time.
     *
     * @param tileIndices An array of <code>Point</code>s representing
     *        tile indices.
     *
     * @return An array of <code>Raster</code>s containing the tiles
     *         corresponding to the given tile indices.
     *
     * @throws IllegalArgumentException  If <code>tileIndices</code> is
     *         <code>null</code>.
     */
    public Raster[] getTiles(Point[] tileIndices) {
        if (tileIndices == null) {
	    throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        int numTiles = tileIndices.length;	// number of tiles requested

        // The requested tiles, to be returned.
        Raster[] tiles = new Raster[numTiles];

        // Indicator for those tiles that actually need to be computed.
        boolean[] computeTiles = new boolean[numTiles];

        int minTileX = getMinTileX();
        int maxTileX = getMaxTileX();
        int minTileY = getMinTileY();
        int maxTileY = getMaxTileY();

        int count = 0;	// number of tiles need to be computed

        for (int i = 0; i < numTiles; i++) {
            int tileX = tileIndices[i].x;
            int tileY = tileIndices[i].y;

            // Make sure the tile is inside image boundary.
            if (tileX >= minTileX && tileX <= maxTileX &&
                tileY >= minTileY && tileY <= maxTileY) {
                // Check if tile is available in the cache.
                tiles[i] = getTileFromCache(tileX, tileY);

                if (tiles[i] == null) {
                    // Tile not in cache. needs computation.
                    computeTiles[i] = true;
                    count++;
                }
            }
        }

        if (count > 0) {	// need to compute some tiles
            if (count == numTiles) {
                // None of the tiles is in cache.
                tiles = scheduler.scheduleTiles(this, tileIndices);

                if (cache != null) {	// cache these tiles
                    if(cache != null) {
                        for (int i = 0; i < numTiles; i++) {
                            cache.add(this,
                                      tileIndices[i].x,
                                      tileIndices[i].y,
                                      tiles[i],
                                      tileCacheMetric);
                        }
                    }
                }

            } else {
                // Only schedule those tiles not in cache for computation.
                Point[] indices = new Point[count];
                count = 0;
                for (int i = 0; i < numTiles; i++) {
                    if (computeTiles[i]) {
                        indices[count++] = tileIndices[i];
                    }
                }

                // Schedule needed tiles and return.
                Raster[] newTiles = scheduler.scheduleTiles(this, indices);

                count = 0;
                for (int i = 0; i < numTiles; i++) {
                    if (computeTiles[i]) {
                        tiles[i] = newTiles[count++];
                        addTileToCache(tileIndices[i].x, tileIndices[i].y,
                                       tiles[i]);
                    }
                }
            }
        }

        return tiles;
    }

    private static TileComputationListener[]
        prependListener(TileComputationListener[] listeners,
                        TileComputationListener listener) {
        if(listeners == null) {
            return new TileComputationListener[] {listener};
        }

        TileComputationListener[] newListeners =
            new TileComputationListener[listeners.length+1];
        newListeners[0] = listener;
        System.arraycopy(listeners, 0, newListeners, 1, listeners.length);

        return newListeners;
    }

    /**
     * Returns an array of indices of tiles which are not cached or
     * <code>null</code> if all are cached.
     */
    /* XXX
    private Point[] pruneIndices(Point[] tileIndices) {
        if(true)return tileIndices;//XXX
        int numIndices = tileIndices.length;

        ArrayList uncachedIndices = new ArrayList(numIndices);

        for(int i = 0; i < numIndices; i++) {
            Point p = tileIndices[i];
            if(getTileFromCache(p.x, p.y) == null) {
                uncachedIndices.add(p);
            }
        }

        int numUncached = uncachedIndices.size();
        return numUncached > 0 ?
            (Point[])uncachedIndices.toArray(new Point[numUncached]) : null;
    }
    */

    /**
     * Queues a list of tiles for computation.  Registered listeners will
     * be notified after each tile has been computed.  The event source
     * parameter passed to such listeners will be the <code>TileScheduler</code>
     * and the image parameter will be this image.
     *
     * @param tileIndices A list of tile indices indicating which tiles
     *        to schedule for computation.
     * @throws IllegalArgumentException  If <code>tileIndices</code> is
     *         <code>null</code>.
     *
     * @since JAI 1.1
     */
    public TileRequest queueTiles(Point[] tileIndices) {
        if (tileIndices == null) {
	    throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        /* XXX bad idea probably
        // Remove any tile indices corresponding to cached tiles.
        tileIndices = pruneIndices(tileIndices);

        // Return if no tiles remain, i.e., all are cached.
        if(tileIndices == null) {
            return;
        }
        */

        // Get registered listeners.
        TileComputationListener[] tileListeners = getTileComputationListeners();

        // Add a listener to cache tiles only if not a SunTileScheduler.
        // The SunTileScheduler caches tiles generated by an OpImage but
        // this is not a requirement of the specification.
        if(!isSunTileScheduler) {
            // Create a local listener.
            TileComputationListener localListener = new TCL(this);

            // Prepend local listener to array.
            tileListeners = prependListener(tileListeners, localListener);
        }

        // Queue the tiles to the scheduler.
        return scheduler.scheduleTiles(this, tileIndices, tileListeners);
    }

    /**
     * Issue an advisory cancellation request to nullify processing of
     * the indicated tiles via the TileScheduler for this image.  This
     * method should merely forward the request to the associated
     * <code>TileScheduler</code>.
     *
     * @param request The request for which tiles are to be cancelled.
     * @param tileIndices The tiles to be cancelled; may be <code>null</code>.
     *        Any tiles not actually in the <code>TileRequest</code> will be
     *        ignored.
     * @throws IllegalArgumentException  If <code>request</code> is
     *         <code>null</code>.
     *
     * @since JAI 1.1
     */
    public void cancelTiles(TileRequest request, Point[] tileIndices) {
        if (request == null) {
	    throw new IllegalArgumentException(JaiI18N.getString("Generic4"));
        }
        scheduler.cancelTiles(request, tileIndices);
    }

    /**
     * Hints that the given tiles might be needed in the near future.
     * Some implementations may spawn one or more threads
     * to compute the tiles, while others may ignore the hint.
     *
     * @param tileIndices A list of tile indices indicating which tiles
     *        to prefetch.
     *
     * @throws IllegalArgumentException  If <code>tileIndices</code> is
     *         <code>null</code>.
     */
    public void prefetchTiles(Point[] tileIndices) {
        if (tileIndices == null) {
	    throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        /* XXX bad idea probably
        // Remove any tile indices corresponding to cached tiles.
        tileIndices = pruneIndices(tileIndices);
        */

        // Return if no tiles remain, i.e., all are cached.
        if(tileIndices == null) {
            return;
        }

        // Prefetch any remaining tiles.
        scheduler.prefetchTiles(this, tileIndices);
    }

    /**
     * Computes the position in the specified source that best
     * matches the supplied destination image position. If it
     * is not possible to compute the requested position,
     * <code>null</code> will be returned. If the point is mapped
     * outside the source bounds, the coordinate value or <code>null</code>
     * may be returned at the discretion of the implementation.
     *
     * <p>Floating-point input and output coordinates are supported,
     * and recommended when possible.  Subclass implementations may
     * however use integer computation if necessary for simplicity.</p>
     *
     * <p>The implementation in this class returns the value of
     * <code>pt</code> in the following code snippet:
     *
     * <pre>
     * Rectangle destRect = new Rectangle((int)destPt.getX(),
     *                                    (int)destPt.getY(),
     *                                    1, 1);
     * Rectangle sourceRect = mapDestRect(destRect, sourceIndex);
     * Point2D pt = (Point2D)destPt.clone();
     * pt.setLocation(sourceRect.x + (sourceRect.width - 1.0)/2.0,
     *                sourceRect.y + (sourceRect.height - 1.0)/2.0);
     * </pre>
     *
     * Subclasses requiring different behavior should override this
     * method.</p>
     *
     * @param destPt the position in destination image coordinates
     * to map to source image coordinates.
     * @param sourceIndex the index of the source image.
     *
     * @return a <code>Point2D</code> of the same class as
     * <code>destPt</code> or <code>null</code>.
     *
     * @throws IllegalArgumentException if <code>destPt</code> is
     * <code>null</code>.
     * @throws IndexOutOfBoundsException if <code>sourceIndex</code> is
     * negative or greater than or equal to the number of sources.
     *
     * @since JAI 1.1.2
     */
    public Point2D mapDestPoint(Point2D destPt, int sourceIndex) {
        if (destPt == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        } else if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            throw new IndexOutOfBoundsException(JaiI18N.getString("Generic1"));
        }

        Rectangle destRect = new Rectangle((int)destPt.getX(),
                                           (int)destPt.getY(),
                                           1, 1);

        Rectangle sourceRect = mapDestRect(destRect, sourceIndex);

        Point2D pt = (Point2D)destPt.clone();
        pt.setLocation(sourceRect.x + (sourceRect.width - 1.0)/2.0,
                       sourceRect.y + (sourceRect.height - 1.0)/2.0);

        return pt;
    }

    /**
     * Computes the position in the destination that best
     * matches the supplied source image position. If it
     * is not possible to compute the requested position,
     * <code>null</code> will be returned. If the point is mapped
     * outside the destination bounds, the coordinate value or
     * <code>null</code> may be returned at the discretion of the
     * implementation.
     *
     * <p>Floating-point input and output coordinates are supported,
     * and recommended when possible.  Subclass implementations may
     * however use integer computation if necessary for simplicity.</p>
     *
     * <p>The implementation in this class returns the value of
     * <code>pt</code> in the following code snippet:
     *
     * <pre>
     * Rectangle sourceRect = new Rectangle((int)sourcePt.getX(),
     *                                      (int)sourcePt.getY(),
     *                                      1, 1);
     * Rectangle destRect = mapSourceRect(sourceRect, sourceIndex);
     * Point2D pt = (Point2D)sourcePt.clone();
     * pt.setLocation(destRect.x + (destRect.width - 1.0)/2.0,
     *                destRect.y + (destRect.height - 1.0)/2.0);
     * </pre>
     *
     * @param sourcePt the position in source image coordinates
     * to map to destination image coordinates.
     * @param sourceIndex the index of the source image.
     *
     * @return a <code>Point2D</code> of the same class as
     * <code>sourcePt</code> or <code>null</code>.
     *
     * @throws IllegalArgumentException if <code>sourcePt</code> is
     * <code>null</code>.
     * @throws IndexOutOfBoundsException if <code>sourceIndex</code> is
     * negative or greater than or equal to the number of sources.
     *
     * @since JAI 1.1.2
     */
    public Point2D mapSourcePoint(Point2D sourcePt, int sourceIndex) {
        if (sourcePt == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        } else if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            throw new IndexOutOfBoundsException(JaiI18N.getString("Generic1"));
        }

        Rectangle sourceRect = new Rectangle((int)sourcePt.getX(),
                                             (int)sourcePt.getY(),
                                             1, 1);

        Rectangle destRect = mapSourceRect(sourceRect, sourceIndex);

        // Return null of destination rectangle is not computable.
        if(destRect == null) {
            return null;
        }

        Point2D pt = (Point2D)sourcePt.clone();
        pt.setLocation(destRect.x + (destRect.width - 1.0)/2.0,
                       destRect.y + (destRect.height - 1.0)/2.0);

        return pt;
    }

    /**
     * Returns a conservative estimate of the destination region that
     * can potentially be affected by the pixels of a rectangle of a
     * given source.  An empty <code>Rectangle</code> may be returned
     * if the destination is unaffected by the contents of the source
     * rectangle.  This is distinct from a <code>null</code> return value
     * which serves rather to indicate that it is not possible to
     * determine the bounds of the affected region.  The safest
     * interpretation of a <code>null</code> return value is that the
     * entire destination might be affected by any pixel within the
     * given source rectangle.
     *
     * @param sourceRect  The <code>Rectangle</code> in source coordinates.
     * @param sourceIndex  The index of the source image.
     *
     * @return A <code>Rectangle</code> indicating the potentially
     *         affected destination region, or <code>null</code> if
     *         the region is unknown.
     *
     * @throws IllegalArgumentException  If the source index is
     *         negative or greater than that of the last source.
     * @throws IllegalArgumentException  If <code>sourceRect</code> is
     *         <code>null</code>.
     */
    public abstract Rectangle mapSourceRect(Rectangle sourceRect,
                                            int sourceIndex);
    
    /**
     * Returns a conservative estimate of the region of a specified
     * source that is required in order to compute the pixels of a
     * given destination rectangle.  The computation may as appropriate
     * clip the mapped <code>Rectangle</code> to the actual bounds of the
     * source or may treat the source as having infinite extent.
     * It is therefore the responsibility of the invoking
     * object to constrain the region in accordance with its needs.
     * Returning an empty <code>Rectangle</code> should indicate that
     * the data of the source image in question are not required for the
     * computation of the specified destination region.  If the entire
     * source image might be required to compute this destination
     * region, then <code>getSourceImage(sourceIndex).getBounds()</code>
     * should be returned.
     *
     * <p> To illustrate the issue of whether the source should be thought
     * to have infinite extent, consider the case wherein computing a
     * destination pixel requires multiple source pixels of context.  At
     * the source image boundary, these pixels might only be available if the
     * source data were extrapolated, e.g., using a {@link BorderExtender}.
     * If such an extender were available, destination pixels could be
     * computed even if they mapped to a region on the source boundary so
     * in this case the source could be considered to have infinite extent.
     * If no such extender were available, only destination pixels with
     * source context contained within the source image bounds could be
     * considered so that it might be preferable to clip the rectangle to
     * the source bounds.</p>
     *
     * @param destRect  The <code>Rectangle</code> in destination coordinates.
     * @param sourceIndex  The index of the source image.
     *
     * @return A non-<code>null</code> <code>Rectangle</code> indicating
     *         the required source region.
     *
     * @throws IllegalArgumentException  If the source index is
     *         negative or greater than that of the last source.
     * @throws IllegalArgumentException  If <code>destRect</code> is
     *         <code>null</code>.
     */
    public abstract Rectangle mapDestRect(Rectangle destRect,
                                          int sourceIndex);

    /**
     * Returns one of <code>OP_COMPUTE_BOUND</code>,
     * <code>OP_IO_BOUND</code>, or <code>OP_NETWORK_BOUND</code> to
     * indicate how the operation is likely to spend its time.  The
     * answer does not affect the output of the operation, but may
     * allow a scheduler to parallelize the computation of multiple
     * operations more effectively.
     *
     * <p> The implementation of this method in this class
     * returns <code>OP_COMPUTE_BOUND</code>.
     */
    public int getOperationComputeType() {
        return OP_COMPUTE_BOUND;
    }
    
    /**
     * Returns <code>true</code> if the <code>OpImage</code> returns an
     * unique <code>Raster</code> object every time <code>computeTile</code>
     * is called.  <code>OpImage</code>s that internally cache
     * <code>Raster</code>s and return them via <code>computeTile</code>
     * should return <code>false</code> for this method.  
     *
     * <p> The implementation of this method in this class always returns
     * <code>true</code>.
     */
    public boolean computesUniqueTiles() {
        return true;
    }

    /**
     * Uncaches all tiles and calls <code>super.dispose()</code>.
     * If a <code>TileRecycler</code> was defined via the configuration
     * variable <code>JAI.KEY_TILE_RECYCLER</code> when this image was
     * constructed and tile recycling was enabled via the configuration
     * variable <code>JAI.KEY_CACHED_TILE_RECYCLING_ENABLED</code>, then each
     * of this image's tiles which is currently in the cache will be
     * recycled.  This method may be invoked more than once although
     * invocations after the first one may do nothing.
     *
     * <p> The results of referencing an image after a call to
     * <code>dispose()</code> are undefined.</p>
     *
     * @since JAI 1.1.2
     */
    public synchronized void dispose() {
        if(isDisposed) {
            return;
        }

        isDisposed = true;

        if (cache != null) {
            if(isCachedTileRecyclingEnabled && tileRecycler != null) {
                Raster[] tiles = cache.getTiles(this);
                if(tiles != null) {
                    int numTiles = tiles.length;
                    for(int i = 0; i < numTiles; i++) {
                        tileRecycler.recycleTile(tiles[i]);
                    }
                }
            }
            cache.removeTiles(this);
        }
        super.dispose();
    }

    /**
     * Indicates whether the source with the given index has a
     * <code>BorderExtender</code>. If the source index is out of bounds
     * for the source vector of this <code>OpImage</code> then an
     * <code>ArrayIndexOutOfBoundsException</code> may be thrown.
     *
     * @param sourceIndex The index of the source in question.
     * @return <code>true</code> if the indicated source has an extender.
     *
     * @deprecated as of JAI 1.1.
     */
    public boolean hasExtender(int sourceIndex) {
        if(sourceIndex != 0) {
            throw new ArrayIndexOutOfBoundsException();
        } else if(this instanceof AreaOpImage) {
            return ((AreaOpImage)this).getBorderExtender() != null;
        } else if(this instanceof GeometricOpImage) {
            return ((GeometricOpImage)this).getBorderExtender() != null;
        }
        return false;
    }

    /**
     * Returns the effective number of bands of an image with a given
     * <code>SampleModel</code> and <code>ColorModel</code>.
     * Normally, this is given by
     * <code>sampleModel.getNumBands()</code>, but for images with an
     * <code>IndexColorModel</code> the effective number of bands is
     * given by <code>colorModel.getNumComponents()</code>, since
     * a single physical sample represents multiple color components.
     *
     * @deprecated as of JAI 1.1.
     */
    public static int getExpandedNumBands(SampleModel sampleModel,
                                          ColorModel colorModel) {
        if (colorModel instanceof IndexColorModel) {
            return colorModel.getNumComponents();
        } else {
            return sampleModel.getNumBands();
        }
    }

    /**
     * Returns the image's format tags to be used with
     * a <code>RasterAccessor</code>.
     *
     * <p> This method will compute and cache the tags the first time
     * it is called on a particular image.  The image's
     * <code>SampleModel</code> and <code>ColorModel</code> must be
     * set to their final values before calling this method.
     *
     * @return An array containing <code>RasterFormatTag</code>s for the
     * sources in the first <code>getNumSources()</code> elements and a
     * <code>RasterFormatTag</code> for the destination in the last element.
     */
    // XXX This method should be removed if we stop using RasterAccessor.
    protected synchronized RasterFormatTag[] getFormatTags() {
        if (formatTags == null) {
            RenderedImage[] sourceArray = new RenderedImage[getNumSources()];
            if(sourceArray.length > 0) {
                getSources().toArray(sourceArray);
            }
            formatTags = RasterAccessor.findCompatibleTags(sourceArray, this);
        }

        return formatTags;
    }

    /**
     * Returns the value of the instance variable <code>tileRecycler</code>.
     *
     * @since JAI 1.1.2
     */
    public TileRecycler getTileRecycler() {
        return tileRecycler;
    }

    /**
     * Creates a <code>WritableRaster</code> at the given tile grid position.
     * The superclass method {@link #createWritableRaster(SampleModel,Point)}
     * will be invoked with this image's <code>SampleModel</code> and the
     * location of the specified tile.
     *
     * <p>Subclasses should ideally use this method to create destination
     * tiles as this method will take advantage of any
     * <code>TileFactory</code> specified to the <code>OpImage</code> at
     * construction.</p>
     *
     * @since JAI 1.1.2
     */
    protected final WritableRaster createTile(int tileX, int tileY) {
        return createWritableRaster(sampleModel,
                                    new Point(tileXToX(tileX),
                                              tileYToY(tileY)));
    }

    /**
     * A tile recycling convenience method.
     *
     * <p>If <code>tileRecycler</code> is non-<code>null</code>, the call
     * is forwarded to {@link TileRecycler.recycleTile(Raster)}; otherwise
     * the method does nothing.</p>
     *
     * <p>This method is for use by subclasses which create
     * <code>Raster</code>s with limited scope which therefore may easily
     * be identified as safe candidates for recycling.  This might occur
     * for example within
     * {@link #computeRect(Raster[],WritableRaster,Rectangle)} or
     * {@link #computeTile(int,int)} wherein <code>Raster</code>s may be
     * created for use within the method but be eligible for garbage
     * collection once the method is exited.</p>
     *
     * @throws IllegalArgumentException if <code>tile</code> is
     *         <code>null</code>.
     *
     * @since JAI 1.1.2
     */
    protected void recycleTile(Raster tile) {
	if (tile == null)
	    throw new IllegalArgumentException(JaiI18N.getString("Generic0"));

        if(tileRecycler != null) {
            tileRecycler.recycleTile(tile);
        }
    }
}
