org.geotools.gce.imagemosaic.RasterLayerResponse.java Source code

Java tutorial

Introduction

Here is the source code for org.geotools.gce.imagemosaic.RasterLayerResponse.java

Source

/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    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.geotools.gce.imagemosaic;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageReadParam;
import javax.measure.unit.Unit;
import javax.media.jai.BorderExtender;
import javax.media.jai.Histogram;
import javax.media.jai.ImageLayout;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.RenderedOp;
import javax.media.jai.TileCache;
import javax.media.jai.TileScheduler;
import javax.media.jai.operator.ConstantDescriptor;
import javax.media.jai.operator.FormatDescriptor;
import javax.media.jai.operator.MosaicDescriptor;
import javax.media.jai.operator.TranslateDescriptor;

import org.apache.commons.io.FilenameUtils;
import org.geotools.coverage.Category;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.TypeMap;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
import org.geotools.data.Query;
import org.geotools.factory.Hints;
import org.geotools.feature.visitor.MaxVisitor;
import org.geotools.filter.IllegalFilterException;
import org.geotools.filter.SortByImpl;
import org.geotools.gce.imagemosaic.GranuleDescriptor.GranuleLoadingResult;
import org.geotools.gce.imagemosaic.ImageMosaicReader.DomainDescriptor;
import org.geotools.gce.imagemosaic.ImageMosaicReader.DomainManager;
import org.geotools.gce.imagemosaic.OverviewsController.OverviewLevel;
import org.geotools.gce.imagemosaic.catalog.GranuleCatalogVisitor;
import org.geotools.gce.imagemosaic.processing.ArtifactsFilterDescriptor;
import org.geotools.geometry.Envelope2D;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.ImageWorker;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.resources.coverage.CoverageUtilities;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.resources.geometry.XRectangle2D;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.image.ImageUtilities;
import org.geotools.util.NumberRange;
import org.geotools.util.SimpleInternationalString;
import org.jaitools.imageutils.ImageLayout2;
import org.jaitools.imageutils.ROIGeometry;
import org.opengis.coverage.ColorInterpretation;
import org.opengis.coverage.SampleDimension;
import org.opengis.coverage.SampleDimensionType;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.opengis.geometry.BoundingBox;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.util.InternationalString;

import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;

/**
 * A RasterLayerResponse. An instance of this class is produced everytime a
 * requestCoverage is called to a reader.
 * 
 * @author Simone Giannecchini, GeoSolutions
 * @author Daniele Romagnoli, GeoSolutions
 * @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties URLs
 */
@SuppressWarnings("rawtypes")
class RasterLayerResponse {

    private static final class SimplifiedGridSampleDimension extends GridSampleDimension
            implements SampleDimension {

        /**
         * 
         */
        private static final long serialVersionUID = 2227219522016820587L;

        private double nodata;
        private double minimum;
        private double maximum;
        private double scale;
        private double offset;
        private Unit<?> unit;
        private SampleDimensionType type;
        private ColorInterpretation color;
        private Category bkg;

        public SimplifiedGridSampleDimension(CharSequence description, SampleDimensionType type,
                ColorInterpretation color, double nodata, double minimum, double maximum, double scale,
                double offset, Unit<?> unit) {
            super(description,
                    !Double.isNaN(nodata)
                            ? new Category[] { new Category(Vocabulary.formatInternational(VocabularyKeys.NODATA),
                                    new Color[] { new Color(0, 0, 0, 0) }, NumberRange.create(nodata, nodata),
                                    NumberRange.create(nodata, nodata)) }
                            : null,
                    unit);
            this.nodata = nodata;
            this.minimum = minimum;
            this.maximum = maximum;
            this.scale = scale;
            this.offset = offset;
            this.unit = unit;
            this.type = type;
            this.color = color;
            this.bkg = new Category("Background", Utils.TRANSPARENT, 0);
        }

        @Override
        public double getMaximumValue() {
            return maximum;
        }

        @Override
        public double getMinimumValue() {
            return minimum;
        }

        @Override
        public double[] getNoDataValues() throws IllegalStateException {
            return new double[] { nodata };
        }

        @Override
        public double getOffset() throws IllegalStateException {
            return offset;
        }

        @Override
        public NumberRange<? extends Number> getRange() {
            return super.getRange();
        }

        @Override
        public SampleDimensionType getSampleDimensionType() {
            return type;
        }

        @Override
        public MathTransform1D getSampleToGeophysics() {
            return super.getSampleToGeophysics();
        }

        @Override
        public Unit<?> getUnits() {
            return unit;
        }

        @Override
        public double getScale() {
            return scale;
        }

        @Override
        public ColorInterpretation getColorInterpretation() {
            return color;
        }

        @Override
        public Category getBackground() {
            return bkg;
        }

        @Override
        public InternationalString[] getCategoryNames() throws IllegalStateException {
            return new InternationalString[] { SimpleInternationalString.wrap("Background") };
        }
    }

    /**
     * My specific {@link MaxVisitor} that keeps track of the feature used for the maximum.
     * @author Simone Giannecchini, GeoSolutions SAS
     */
    static class MaxVisitor2 extends MaxVisitor {

        private Comparable oldValue;
        private int oldNanCount;
        private int oldNullCount;

        private Feature targetFeature = null;

        public MaxVisitor2(Expression expr) throws IllegalFilterException {
            super(expr);
        }

        public MaxVisitor2(int attributeTypeIndex, SimpleFeatureType type) throws IllegalFilterException {
            super(attributeTypeIndex, type);
        }

        public Feature getTargetFeature() {
            return targetFeature;
        }

        public MaxVisitor2(String attrName, SimpleFeatureType type) throws IllegalFilterException {
            super(attrName, type);
        }

        public MaxVisitor2(String attributeTypeName) {
            super(attributeTypeName);
        }

        @Override
        public void reset() {
            super.reset();
            this.oldValue = null;
            this.targetFeature = null;
        }

        @Override
        public void setValue(Object result) {
            super.setValue(result);
            this.oldValue = null;
            this.targetFeature = null;
        }

        @SuppressWarnings("unchecked")
        @Override
        public void visit(Feature feature) {
            super.visit(feature);
            // if we got a NAN let's leave
            final int nanCount = getNaNCount();
            if (oldNanCount != nanCount) {
                oldNanCount = nanCount;
                return;
            }

            // if we got a null let's leave         
            final int nullCount = getNullCount();
            if (oldNullCount != nullCount) {
                oldNullCount = nullCount;
                return;
            }

            // check if we got a real value
            final Comparable max = getMax();
            if (oldValue == null || max.compareTo(oldValue) != 0) {
                targetFeature = feature;
                oldValue = max;
            }
        }

    }

    /**
     * This class is responsible for putting together the granules for the final mosaic.
     * 
     * @author Simone Giannecchini, GeoSolutions SAS
     *
     */
    class MosaicBuilder implements GranuleCatalogVisitor {

        private final int maxNumberOfGranules;
        private final List<Future<GranuleLoadingResult>> tasks = new ArrayList<Future<GranuleLoadingResult>>();
        private int granulesNumber;
        private List<ROI> rois = new ArrayList<ROI>();
        private Color inputTransparentColor;
        private PlanarImage[] alphaChannels;
        private RasterLayerRequest request;

        private ROI[] sourceRoi;

        private double[][] sourceThreshold;

        private boolean doInputTransparency;

        private List<RenderedImage> sources = new ArrayList<RenderedImage>();

        /**
        * Default {@link Constructor}
        */
        public MosaicBuilder(final RasterLayerRequest request) {
            this.request = request;
            maxNumberOfGranules = request.getMaximumNumberOfGranules();
        }

        public RenderedImage[] getSourcesAsArray() {
            RenderedImage[] imageSources = new RenderedImage[sources.size()];
            sources.toArray(imageSources);
            return imageSources;
        }

        public void visit(GranuleDescriptor granuleDescriptor, Object o) {
            // don't collect more than the specified amount of granules
            // SG20092011 this might not happen since we set the max features in the query, but 
            // who knows!
            //           if(maxNumberOfGranules>0 &&granulesNumber >=maxNumberOfGranules) {
            //                return;
            //            }

            //
            // load raster data
            //
            // create a granuleDescriptor loader
            final Geometry bb = JTS.toGeometry((BoundingBox) mosaicBBox);
            final Geometry inclusionGeometry = granuleDescriptor.inclusionGeometry;
            if (!footprintManagement || inclusionGeometry == null
                    || footprintManagement && inclusionGeometry.intersects(bb)) {
                final GranuleLoader loader = new GranuleLoader(baseReadParameters, imageChoice, mosaicBBox,
                        finalWorldToGridCorner, granuleDescriptor, request, hints);
                if (multithreadingAllowed && rasterManager.parent.multiThreadedLoader != null)
                    tasks.add(rasterManager.parent.multiThreadedLoader.submit(loader));
                else
                    tasks.add(new FutureTask<GranuleLoadingResult>(loader));

                granulesNumber++;
            }
        }

        public void produce() {

            // reusable parameters
            alphaChannels = new PlanarImage[granulesNumber];
            int granuleIndex = 0;
            inputTransparentColor = request.getInputTransparentColor();
            doInputTransparency = inputTransparentColor != null && !footprintManagement;
            // execute them all
            boolean firstGranule = true;
            int[] alphaIndex = null;
            StringBuilder paths = new StringBuilder();

            for (Future<GranuleLoadingResult> future : tasks) {

                final RenderedImage loadedImage;
                final GranuleLoadingResult result;
                final URL url;
                final File inputFile;
                final String canonicalPath;
                boolean doFiltering;
                try {
                    if (!multithreadingAllowed || rasterManager.parent.multiThreadedLoader == null) {
                        //run the loading in this thread
                        final FutureTask<GranuleLoadingResult> task = (FutureTask<GranuleLoadingResult>) future;
                        task.run();
                    }
                    result = future.get();
                    if (result == null) {
                        if (LOGGER.isLoggable(Level.FINE))
                            LOGGER.log(Level.FINE, "Unable to load the raster for granule " + granuleIndex
                                    + " with request " + request.toString());
                        continue;
                    }
                    loadedImage = result.getRaster();
                    doFiltering = result.isDoFiltering();
                    url = result.granuleUrl;
                    inputFile = DataUtilities.urlToFile(url);
                    canonicalPath = inputFile.getCanonicalPath();
                    if (loadedImage == null) {
                        if (LOGGER.isLoggable(Level.FINE))
                            LOGGER.log(Level.FINE, "Unable to load the raster for granuleDescriptor " + granuleIndex
                                    + " with request " + request.toString());
                        continue;
                    }
                    if (firstGranule) {
                        //
                        // We check here if the images have an alpha channel or some
                        // other sort of transparency. In case we have transparency
                        // I also save the index of the transparent channel.
                        //
                        // Specifically, I have to check if the loaded image have
                        // transparency, because if we do a ROI and/or we have a
                        // transparent color to set we have to remove it.
                        //
                        final ColorModel cm = loadedImage.getColorModel();
                        alphaIn = cm.hasAlpha();
                        if (alphaIn || doInputTransparency)
                            alphaIndex = new int[] { cm.getNumComponents() - 1 };

                        //
                        // we set the input threshold accordingly to the input
                        // image data type. I find the default value (which is 0) very bad
                        // for data type other than byte and ushort. With float and double
                        // it can cut off a large par of the dynamic.
                        //
                        sourceThreshold = new double[][] { { CoverageUtilities
                                .getMosaicThreshold(loadedImage.getSampleModel().getDataType()) } };

                        firstGranule = false;

                    }

                } catch (Exception e) {
                    if (LOGGER.isLoggable(Level.FINE))
                        LOGGER.fine("Adding to mosaic image number " + granuleIndex
                                + " failed, original request was " + request);
                    continue;
                }

                if (LOGGER.isLoggable(Level.FINER)) {
                    LOGGER.finer("Adding to mosaic image number " + granuleIndex);
                }

                //
                // add to the mosaic collection, with preprocessing
                //
                RenderedImage raster = processGranuleRaster(loadedImage, granuleIndex, alphaIndex, alphaIn,
                        alphaChannels, doInputTransparency, inputTransparentColor);

                // we need to add its roi in order to avoid problems with the mosaic overlapping
                Rectangle bounds = PlanarImage.wrapRenderedImage(raster).getBounds();
                Geometry mask = JTS.toGeometry(
                        new Envelope(bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY()));
                ROI imageBounds = new ROIGeometry(mask);
                if (footprintManagement) {
                    final ROI footprint = result.getFootprint();
                    if (footprint != null) {
                        if (imageBounds.contains(footprint.getBounds2D().getBounds())) {
                            imageBounds = footprint;
                        } else {
                            imageBounds = imageBounds.intersect(footprint);
                        }
                    }

                    //Artifacts filtering processing
                    if (defaultArtifactsFilterThreshold != Integer.MIN_VALUE && doFiltering) {
                        int artifactThreshold = defaultArtifactsFilterThreshold;
                        if (artifactsFilterPTileThreshold != -1) {

                            //Looking for a histogram for that granule in order to 
                            //setup dynamic threshold 
                            if (url != null) {
                                final String path = FilenameUtils.getFullPath(canonicalPath);
                                final String baseName = FilenameUtils.getBaseName(canonicalPath);
                                final String histogramPath = path + baseName + "." + "histogram";
                                final Histogram histogram = Utils.getHistogram(histogramPath);
                                if (histogram != null) {
                                    final double[] p = histogram.getPTileThreshold(artifactsFilterPTileThreshold);
                                    artifactThreshold = (int) p[0];
                                }
                            }
                        }
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Filtering granules artifacts");
                        }
                        raster = ArtifactsFilterDescriptor.create(raster, imageBounds, new double[] { 0 },
                                artifactThreshold, 3, hints);
                    }
                }
                rois.add(imageBounds);

                // add to mosaic
                sources.add(raster);
                paths.append(granuleIndex > 0 ? "," : "").append(canonicalPath);

                //increment index 
                granuleIndex++;
            }

            granulesNumber = granuleIndex;
            if (granulesNumber == 0) {
                if (LOGGER.isLoggable(Level.FINE))
                    LOGGER.log(Level.FINE, "Unable to load any granuleDescriptor ");
                return;
            }
            granulesPaths = paths.toString();
            sourceRoi = rois.toArray(new ROI[rois.size()]);
        }

    }

    /** Logger. */
    private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(RasterLayerResponse.class);

    /**
     * The GridCoverage produced after a {@link #compute()} method call
     */
    private GridCoverage2D gridCoverage;

    /** The {@link RasterLayerRequest} originating this response */
    private RasterLayerRequest request;

    /** The coverage factory producing a {@link GridCoverage} from an image */
    private GridCoverageFactory coverageFactory;

    /** The base envelope related to the input coverage */
    private GeneralEnvelope coverageEnvelope;

    private RasterManager rasterManager;

    private Color finalTransparentColor;

    private ReferencedEnvelope mosaicBBox;

    private Rectangle rasterBounds;

    private MathTransform2D finalGridToWorldCorner;

    private MathTransform2D finalWorldToGridCorner;

    private int imageChoice = 0;

    private ImageReadParam baseReadParameters = new ImageReadParam();

    private boolean multithreadingAllowed = false;

    private boolean footprintManagement = !Utils.IGNORE_FOOTPRINT;

    private int defaultArtifactsFilterThreshold = Integer.MIN_VALUE;

    private double artifactsFilterPTileThreshold = ImageMosaicFormat.DEFAULT_ARTIFACTS_FILTER_PTILE_THRESHOLD;

    private boolean setRoiProperty;

    private boolean alphaIn = false;

    private boolean oversampledRequest = false;

    private MathTransform baseGridToWorld;

    private Interpolation interpolation;

    private boolean needsReprojection;

    private double[] backgroundValues;

    private Hints hints;

    private String granulesPaths;

    /**
     * Construct a {@code RasterLayerResponse} given a specific
     * {@link RasterLayerRequest}, a {@code GridCoverageFactory} to produce
     * {@code GridCoverage}s and an {@code ImageReaderSpi} to be used for
     * instantiating an Image Reader for a read operation,
     * 
     * @param request
     *            a {@link RasterLayerRequest} originating this response.
     * @param coverageFactory
     *            a {@code GridCoverageFactory} to produce a {@code
     *            GridCoverage} when calling the {@link #compute()} method.
     * @param readerSpi
     *            the Image Reader Service provider interface.
     */
    public RasterLayerResponse(final RasterLayerRequest request, final RasterManager rasterManager) {
        this.request = request;
        coverageEnvelope = rasterManager.spatialDomainManager.coverageEnvelope;
        this.coverageFactory = rasterManager.getCoverageFactory();
        this.rasterManager = rasterManager;
        this.hints = rasterManager.getHints();
        baseGridToWorld = rasterManager.spatialDomainManager.coverageGridToWorld2D;
        finalTransparentColor = request.getOutputTransparentColor();
        // are we doing multithreading?
        multithreadingAllowed = request.isMultithreadingAllowed();
        footprintManagement = request.isFootprintManagement();
        setRoiProperty = request.isSetRoiProperty();
        backgroundValues = request.getBackgroundValues();
        interpolation = request.getInterpolation();
        needsReprojection = request.isNeedsReprojection();
        defaultArtifactsFilterThreshold = request.getDefaultArtifactsFilterThreshold();
        artifactsFilterPTileThreshold = request.getArtifactsFilterPTileThreshold();
    }

    /**
     * Compute the coverage request and produce a grid coverage which will be
     * returned by {@link #createResponse()}. The produced grid coverage may be
     * {@code null} in case of empty request.
     * 
     * @return the {@link GridCoverage} produced as computation of this response
     *         using the {@link #compute()} method.
     * @throws IOException
     * @uml.property name="gridCoverage"
     */
    public GridCoverage2D createResponse() throws IOException {
        processRequest();
        return gridCoverage;
    }

    /**
     * @return the {@link RasterLayerRequest} originating this response.
     * 
     * @uml.property name="request"
     */
    public RasterLayerRequest getOriginatingCoverageRequest() {
        return request;
    }

    /**
     * This method creates the GridCoverage2D from the underlying file given a
     * specified envelope, and a requested dimension.
     * 
     * @param iUseJAI
     *            specify if the underlying read process should leverage on a
     *            JAI ImageRead operation or a simple direct call to the {@code
     *            read} method of a proper {@code ImageReader}.
     * @param overviewPolicy
     *            the overview policy which need to be adopted
     * @return a {@code GridCoverage}
     * 
     * @throws java.io.IOException
     */
    private void processRequest() throws IOException {

        if (request.isEmpty()) {
            if (LOGGER.isLoggable(Level.FINE))
                LOGGER.log(Level.FINE, "Request is empty: " + request.toString());
            this.gridCoverage = null;
            return;
        }

        // assemble granules
        final RenderedImage mosaic = prepareResponse();
        if (mosaic == null) {
            this.gridCoverage = null;
            return;
        }

        //postproc
        RenderedImage finalRaster = postProcessRaster(mosaic);
        //create the coverage
        gridCoverage = prepareCoverage(finalRaster);

    }

    private RenderedImage postProcessRaster(RenderedImage image) {
        // alpha on the final mosaic
        if (finalTransparentColor != null) {
            if (LOGGER.isLoggable(Level.FINE))
                LOGGER.fine("Support for alpha on final mosaic");
            return ImageUtilities.maskColor(finalTransparentColor, image);

        }
        if (!needsReprojection) {
            try {

                // creating source grid to world corrected to the pixel corner
                final AffineTransform sourceGridToWorld = new AffineTransform(
                        (AffineTransform) finalGridToWorldCorner);

                // target world to grid at the corner
                final AffineTransform targetGridToWorld = new AffineTransform(request.getRequestedGridToWorld());
                targetGridToWorld.concatenate(CoverageUtilities.CENTER_TO_CORNER);

                // target world to grid at the corner
                final AffineTransform targetWorldToGrid = targetGridToWorld.createInverse();
                // final complete transformation
                targetWorldToGrid.concatenate(sourceGridToWorld);

                //update final grid to world
                finalGridToWorldCorner = new AffineTransform2D(targetGridToWorld);
                //
                // Check and see if the affine transform is doing a copy.
                // If so call the copy operation.
                //
                // we are in raster space here, so 1E-3 is safe
                if (XAffineTransform.isIdentity(targetWorldToGrid, Utils.AFFINE_IDENTITY_EPS))
                    return image;

                // create final image
                // TODO this one could be optimized further depending on how the affine is created
                //
                // In case we are asked to use certain tile dimensions we tile
                // also at this stage in case the read type is Direct since
                // buffered images comes up untiled and this can affect the
                // performances of the subsequent affine operation.
                //
                final Hints localHints = new Hints(hints);
                if (hints != null && !hints.containsKey(JAI.KEY_BORDER_EXTENDER)) {
                    final Object extender = hints.get(JAI.KEY_BORDER_EXTENDER);
                    if (!(extender != null && extender instanceof BorderExtender)) {
                        localHints.add(ImageUtilities.EXTEND_BORDER_BY_COPYING);
                    }
                }

                //                         image = AffineDescriptor.create(image, targetWorldToGrid , interpolation, backgroundValues, localHints);
                ImageWorker iw = new ImageWorker(image);
                iw.setRenderingHints(localHints);
                iw.affine(targetWorldToGrid, interpolation, backgroundValues);
                image = iw.getRenderedImage();
            } catch (NoninvertibleTransformException e) {
                if (LOGGER.isLoggable(Level.SEVERE)) {
                    LOGGER.log(Level.SEVERE, "Unable to create the requested mosaic ", e);
                }
            }
        }
        return image;
    }

    /**
     * This method loads the granules which overlap the requested
     * {@link GeneralEnvelope} using the provided values for alpha and input
     * ROI.
     * @return
     * @throws DataSourceException
     */
    private RenderedImage prepareResponse() throws DataSourceException {

        try {

            //
            // prepare the params for executing a mosaic operation.
            //
            // It might important to set the mosaic type to blend otherwise
            // sometimes strange results jump in.

            // select the relevant overview, notice that at this time we have
            // relaxed a bit the requirement to have the same exact resolution
            // for all the overviews, but still we do not allow for reading the
            // various grid to world transform directly from the input files,
            // therefore we are assuming that each granuleDescriptor has a scale and
            // translate only grid to world that can be deduced from its base
            // level dimension and envelope. The grid to world transforms for
            // the other levels can be computed accordingly knowing the scale
            // factors.
            if (request.getRequestedBBox() != null && request.getRequestedRasterArea() != null
                    && !request.isHeterogeneousGranules())
                imageChoice = ReadParamsController.setReadParams(request.getRequestedResolution(),
                        request.getOverviewPolicy(), request.getDecimationPolicy(), baseReadParameters,
                        request.rasterManager, request.rasterManager.overviewsController); // use general overviews controller
            else
                imageChoice = 0;
            assert imageChoice >= 0;
            if (LOGGER.isLoggable(Level.FINE))
                LOGGER.fine(new StringBuffer("Loading level ").append(imageChoice)
                        .append(" with subsampling factors ").append(baseReadParameters.getSourceXSubsampling())
                        .append(" ").append(baseReadParameters.getSourceYSubsampling()).toString());

            // ok we got something to return, let's load records from the index
            final BoundingBox cropBBOX = request.getCropBBox();
            if (cropBBOX != null)
                mosaicBBox = ReferencedEnvelope.reference(cropBBOX);
            else
                mosaicBBox = new ReferencedEnvelope(coverageEnvelope);

            //compute final world to grid
            // base grid to world for the center of pixels
            final AffineTransform g2w;
            final OverviewLevel baseLevel = rasterManager.overviewsController.resolutionsLevels.get(0);
            final OverviewLevel selectedLevel = rasterManager.overviewsController.resolutionsLevels
                    .get(imageChoice);
            final double resX = baseLevel.resolutionX;
            final double resY = baseLevel.resolutionY;
            final double[] requestRes = request.getRequestedResolution();

            g2w = new AffineTransform((AffineTransform) baseGridToWorld);
            g2w.concatenate(CoverageUtilities.CENTER_TO_CORNER);

            if ((requestRes[0] < resX || requestRes[1] < resY)) {
                // Using the best available resolution
                oversampledRequest = true;
            } else {

                // SG going back to working on a per level basis to do the composition
                // g2w = new AffineTransform(request.getRequestedGridToWorld());
                g2w.concatenate(
                        AffineTransform.getScaleInstance(selectedLevel.scaleFactor, selectedLevel.scaleFactor));
                g2w.concatenate(AffineTransform.getScaleInstance(baseReadParameters.getSourceXSubsampling(),
                        baseReadParameters.getSourceYSubsampling()));
            }

            // move it to the corner
            finalGridToWorldCorner = new AffineTransform2D(g2w);
            finalWorldToGridCorner = finalGridToWorldCorner.inverse();// compute raster bounds
            final GeneralEnvelope tempRasterBounds = CRS.transform(finalWorldToGridCorner, mosaicBBox);
            rasterBounds = tempRasterBounds.toRectangle2D().getBounds();

            //          SG using the above may lead to problems since the reason is that  may be a little (1 px) bigger
            //          than what we need. The code below is a bit better since it uses a proper logic (see GridEnvelope
            //          Javadoc)
            //         rasterBounds = new GridEnvelope2D(new Envelope2D(tempRasterBounds), PixelInCell.CELL_CORNER);
            if (rasterBounds.width == 0)
                rasterBounds.width++;
            if (rasterBounds.height == 0)
                rasterBounds.height++;
            if (oversampledRequest)
                rasterBounds.grow(2, 2);

            // make sure we do not go beyond the raster dimensions for this layer
            final GeneralEnvelope levelRasterArea_ = CRS.transform(finalWorldToGridCorner,
                    rasterManager.spatialDomainManager.coverageBBox);
            final GridEnvelope2D levelRasterArea = new GridEnvelope2D(new Envelope2D(levelRasterArea_),
                    PixelInCell.CELL_CORNER);
            XRectangle2D.intersect(levelRasterArea, rasterBounds, rasterBounds);

            // create the index visitor and visit the feature
            final MosaicBuilder visitor = new MosaicBuilder(request);
            final List times = request.getRequestedTimes();
            final List elevations = request.getElevation();
            final Map<String, List> additionalDomains = request.getRequestedAdditionalDomains();
            final Filter filter = request.getFilter();
            final boolean hasTime = (times != null && times.size() > 0);
            final boolean hasElevation = (elevations != null && elevations.size() > 0);
            final boolean hasAdditionalDomains = additionalDomains.size() > 0;
            final boolean hasFilter = filter != null && !Filter.INCLUDE.equals(filter);

            // create query
            final SimpleFeatureType type = rasterManager.granuleCatalog.getType();
            Query query = null;
            Filter bbox = null;
            if (type != null) {
                query = new Query(rasterManager.granuleCatalog.getType().getTypeName());
                bbox = FeatureUtilities.DEFAULT_FILTER_FACTORY.bbox(
                        FeatureUtilities.DEFAULT_FILTER_FACTORY
                                .property(rasterManager.granuleCatalog.getType().getGeometryDescriptor().getName()),
                        mosaicBBox);
                query.setFilter(bbox);
            } else {
                throw new IllegalStateException("GranuleCatalog feature type was null!!!");
            }

            // prepare eventual filter for filtering granules
            // handle elevation indexing first since we then combine this with the max in case we are asking for current in time
            if (hasElevation) {

                final Filter elevationF = rasterManager.parent.elevationDomainManager
                        .createFilter(ImageMosaicReader.ELEVATION_DOMAIN, elevations);
                query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), elevationF));
            }

            // handle generic filter since we then combine this with the max in case we are asking for current in time
            if (hasFilter) {
                query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), filter));
            }

            // fuse time query with the bbox query
            if (hasTime) {
                final Filter timeFilter = this.rasterManager.parent.timeDomainManager
                        .createFilter(ImageMosaicReader.TIME_DOMAIN, times);
                query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), timeFilter));
            }

            if (hasAdditionalDomains) {
                final List<Filter> additionalFilter = new ArrayList<Filter>();
                for (Entry<String, List> entry : additionalDomains.entrySet()) {

                    // build a filter for each dimension
                    final String domainName = entry.getKey() + DomainDescriptor.DOMAIN_SUFFIX;
                    additionalFilter.add(
                            rasterManager.parent.domainsManager.createFilter(domainName, (List) entry.getValue()));

                }
                // merge with existing ones
                query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(),
                        FeatureUtilities.DEFAULT_FILTER_FACTORY.and(additionalFilter)));
            }

            //
            // handle secondary query parameters
            //

            // max number of elements
            if (request.getMaximumNumberOfGranules() > 0) {
                query.setMaxFeatures(request.getMaximumNumberOfGranules());
            }

            // sort by clause
            final String sortByClause = request.getSortClause();
            if (sortByClause != null && sortByClause.length() > 0) {
                final String[] elements = sortByClause.split(",");
                if (elements != null && elements.length > 0) {
                    final List<SortBy> clauses = new ArrayList<SortBy>(elements.length);
                    for (String element : elements) {
                        // check
                        if (element == null || element.length() <= 0) {
                            continue;// next, please!
                        }
                        try {
                            // which clause?
                            // ASCENDING
                            element = element.trim();
                            if (element.endsWith(Utils.ASCENDING_ORDER_IDENTIFIER)) {
                                String attribute = element.substring(0, element.length() - 2);
                                clauses.add(
                                        new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute),
                                                SortOrder.ASCENDING));
                            } else
                            // DESCENDING
                            if (element.contains(Utils.DESCENDING_ORDER_IDENTIFIER)) {
                                String attribute = element.substring(0, element.length() - 2);
                                clauses.add(
                                        new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute),
                                                SortOrder.DESCENDING));
                            }
                            //                        if(element.startsWith(Utils.ASCENDING_ORDER_IDENTIFIER)){
                            //                           String attribute=element.substring(Utils.ASCENDING_ORDER_IDENTIFIER.length()+1);
                            //                           attribute=attribute.substring(0, attribute.length()-1);
                            //                           clauses.add(new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute),SortOrder.ASCENDING));
                            //                        } else 
                            //                           // DESCENDING
                            //                           if(element.startsWith(Utils.DESCENDING_ORDER_IDENTIFIER)){
                            //                               String attribute=element.substring(Utils.DESCENDING_ORDER_IDENTIFIER.length()+1);
                            //                               attribute=attribute.substring(0, attribute.length()-1);
                            //                               clauses.add(new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute),SortOrder.DESCENDING));
                            //                        } else {
                            else {
                                if (LOGGER.isLoggable(Level.FINE)) {
                                    LOGGER.fine("Ignoring sort clause :" + element);
                                }
                            }
                        } catch (Exception e) {
                            if (LOGGER.isLoggable(Level.INFO)) {
                                LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
                            }
                        }
                    }

                    // assign to query if sorting is supported!
                    final SortBy[] sb = clauses.toArray(new SortBy[] {});
                    if (rasterManager.granuleCatalog.getQueryCapabilities().supportsSorting(sb)) {
                        query.setSortBy(sb);
                    }
                }
            }

            // collect granules
            rasterManager.getGranules(query, visitor);

            // get those granules
            visitor.produce();

            //
            // Did we actually load anything?? Notice that it might happen that
            // either we have holes inside the definition area for the mosaic
            // or we had some problem with missing tiles, therefore it might
            // happen that for some bboxes we don't have anything to load.
            //
            RenderedImage returnValue = null;
            if (visitor.granulesNumber >= 1) {

                //
                // Create the mosaic image by doing a crop if necessary and also
                // managing the transparent color if applicable. Be aware that
                // management of the transparent color involves removing
                // transparency information from the input images.
                //          
                returnValue = buildMosaic(visitor);
                if (returnValue != null) {
                    if (LOGGER.isLoggable(Level.FINE))
                        LOGGER.fine("Loaded bbox " + mosaicBBox.toString() + " while crop bbox "
                                + request.getCropBBox().toString());
                    return returnValue;
                }

            }

            // Redo the query without filter to check whether we got no granules due
            // to a filter. In that case we need to return null
            if (hasTime || hasElevation || hasFilter || hasAdditionalDomains) {
                query.setFilter(bbox);
                rasterManager.getGranules(query, visitor);
                // get those granules
                visitor.produce();
                if (visitor.granulesNumber >= 1) {
                    // It means the previous lack of granule was due to a filter excluding all the results. Then we return null
                    return null;
                }
            }

            if (LOGGER.isLoggable(Level.FINE))
                LOGGER.fine("Creating constant image for area with no data");

            // if we get here that means that we do not have anything to load
            // but still we are inside the definition area for the mosaic,
            // therefore we create a fake coverage using the background values,
            // if provided (defaulting to 0), as well as the compute raster
            // bounds, envelope and grid to world.

            final Number[] values = ImageUtilities.getBackgroundValues(rasterManager.defaultSM, backgroundValues);
            // create a constant image with a proper layout
            RenderedImage finalImage = ConstantDescriptor.create(Float.valueOf(rasterBounds.width),
                    Float.valueOf(rasterBounds.height), values, null);
            if (rasterBounds.x != 0 || rasterBounds.y != 0) {
                finalImage = TranslateDescriptor.create(finalImage, Float.valueOf(rasterBounds.x),
                        Float.valueOf(rasterBounds.y), Interpolation.getInstance(Interpolation.INTERP_NEAREST),
                        null);
            }
            if (rasterManager.defaultCM != null) {
                final ImageLayout2 il = new ImageLayout2();
                il.setColorModel(rasterManager.defaultCM);
                Dimension tileSize = request.getTileDimensions();
                if (tileSize == null) {
                    tileSize = JAI.getDefaultTileSize();
                }
                il.setSampleModel(
                        rasterManager.defaultCM.createCompatibleSampleModel(tileSize.width, tileSize.height));
                il.setTileGridXOffset(0).setTileGridYOffset(0).setTileWidth((int) tileSize.getWidth())
                        .setTileHeight((int) tileSize.getHeight());
                return FormatDescriptor.create(finalImage, Integer.valueOf(il.getSampleModel(null).getDataType()),
                        new RenderingHints(JAI.KEY_IMAGE_LAYOUT, il));
            }
            return finalImage;

        } catch (Exception e) {
            throw new DataSourceException("Unable to create this mosaic", e);
        }
    }

    private RenderedImage processGranuleRaster(RenderedImage granule, final int granuleIndex,
            final int[] alphaIndex, final boolean alphaIn, final PlanarImage[] alphaChannels,
            final boolean doTransparentColor, final Color transparentColor) {

        //
        // INDEX COLOR MODEL EXPANSION
        //
        // Take into account the need for an expansions of the original color
        // model.
        //
        // If the original color model is an index color model an expansion
        // might be requested in case the different palettes are not all the
        // same. In this case the mosaic operator from JAI would provide wrong
        // results since it would take the first palette and use that one for
        // all the other images.
        //
        // There is a special case to take into account here. In case the input
        // images use an IndexColorModel it might happen that the transparent
        // color is present in some of them while it is not present in some
        // others. This case is the case where for sure a color expansion is
        // needed. However we have to take into account that during the masking
        // phase the images where the requested transparent color was present
        // will have 4 bands, the other 3. If we want the mosaic to work we
        // have to add an extra band to the latter type of images for providing
        // alpha information to them.
        //
        //
        if (rasterManager.expandMe && granule.getColorModel() instanceof IndexColorModel) {
            granule = new ImageWorker(granule).forceComponentColorModel().getRenderedImage();
        }

        //
        // TRANSPARENT COLOR MANAGEMENT
        //
        if (doTransparentColor) {
            if (LOGGER.isLoggable(Level.FINE))
                LOGGER.fine("Support for alpha on input image number " + granuleIndex);
            granule = ImageUtilities.maskColor(transparentColor, granule);
            alphaIndex[0] = granule.getColorModel().getNumComponents() - 1;
        }
        //
        // ROI
        //
        if (alphaIn || doTransparentColor) {
            ImageWorker w = new ImageWorker(granule);
            if (granule.getSampleModel() instanceof MultiPixelPackedSampleModel)
                w.forceComponentColorModel();
            //
            // ALPHA in INPUT
            //
            // I have to select the alpha band and provide it to the final
            // mosaic operator. I have to force going to ComponentColorModel in
            // case the image is indexed.
            //
            if (granule.getColorModel() instanceof IndexColorModel) {
                alphaChannels[granuleIndex] = w.forceComponentColorModel().retainLastBand().getPlanarImage();
            } else
                alphaChannels[granuleIndex] = w.retainBands(alphaIndex).getPlanarImage();

        }

        return granule;

    }

    /**
     * Once we reach this method it means that we have loaded all the images
     * which were intersecting the requested envelope. Next step is to create
     * the final mosaic image and cropping it to the exact requested envelope.
     * @param visitor 
     * 
     * @return A {@link RenderedImage}}.
     */
    private RenderedImage buildMosaic(final MosaicBuilder visitor) throws IOException {

        // build final layout and use it for cropping purposes
        final ImageLayout layout = new ImageLayout(rasterBounds.x, rasterBounds.y, rasterBounds.width,
                rasterBounds.height);

        //prepare hints
        final Dimension tileDimensions = request.getTileDimensions();
        if (tileDimensions != null) {
            layout.setTileHeight(tileDimensions.width).setTileWidth(tileDimensions.height);
        }
        final RenderingHints localHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout);
        if (hints != null && !hints.isEmpty()) {
            if (hints.containsKey(JAI.KEY_TILE_CACHE)) {
                final Object tc = hints.get(JAI.KEY_TILE_CACHE);
                if (tc != null && tc instanceof TileCache)
                    localHints.add(new RenderingHints(JAI.KEY_TILE_CACHE, (TileCache) tc));
            }
            boolean addBorderExtender = true;
            if (hints != null && hints.containsKey(JAI.KEY_BORDER_EXTENDER)) {
                final Object extender = hints.get(JAI.KEY_BORDER_EXTENDER);
                if (extender != null && extender instanceof BorderExtender) {
                    localHints.add(new RenderingHints(JAI.KEY_BORDER_EXTENDER, (BorderExtender) extender));
                    addBorderExtender = false;
                }
            }
            if (addBorderExtender) {
                localHints.add(ImageUtilities.BORDER_EXTENDER_HINTS);
            }
            if (hints.containsKey(JAI.KEY_TILE_SCHEDULER)) {
                final Object ts = hints.get(JAI.KEY_TILE_SCHEDULER);
                if (ts != null && ts instanceof TileScheduler)
                    localHints.add(new RenderingHints(JAI.KEY_TILE_SCHEDULER, (TileScheduler) ts));
            }
        }

        //
        // SPECIAL CASE
        // 1 single tile, we try not do a mosaic.
        final ROI[] sourceRoi = visitor.sourceRoi;
        if (visitor.granulesNumber == 1 && Utils.OPTIMIZE_CROP) {
            // the roi is exactly equal to the 
            final ROI roi = visitor.rois.get(0);
            Rectangle bounds = Utils.toRectangle(roi.getAsShape());
            if (bounds != null) {
                RenderedImage image = visitor.getSourcesAsArray()[0];
                Rectangle imageBounds = PlanarImage.wrapRenderedImage(image).getBounds();
                if (imageBounds.equals(bounds)) {

                    // do we need to crop? (image is bigger than requested?)
                    if (!rasterBounds.contains(imageBounds)) {
                        // we have to crop
                        XRectangle2D.intersect(imageBounds, rasterBounds, imageBounds);

                        if (imageBounds.isEmpty()) {
                            // return back a constant image
                            return null;
                        }
                        // crop
                        ImageWorker iw = new ImageWorker(image);
                        iw.setRenderingHints(localHints);
                        iw.crop(imageBounds.x, imageBounds.y, imageBounds.width, imageBounds.height);

                        image = iw.getRenderedImage();
                        imageBounds = PlanarImage.wrapRenderedImage(image).getBounds();
                    }

                    // and, do we need to add a border around the image?
                    if (!imageBounds.contains(rasterBounds)) {
                        image = MosaicDescriptor.create(new RenderedImage[] { image },
                                request.isBlend() ? MosaicDescriptor.MOSAIC_TYPE_BLEND
                                        : MosaicDescriptor.MOSAIC_TYPE_OVERLAY,
                                (alphaIn || visitor.doInputTransparency) ? visitor.alphaChannels : null, sourceRoi,
                                visitor.sourceThreshold, backgroundValues, localHints);
                    }

                    return image;
                }
            }
        }

        // 
        // Final Merge
        // 
        // I can even do a stacking merge or a flat merge
        final RenderedImage mosaic = request.getMergeBehavior().process(visitor.getSourcesAsArray(),
                backgroundValues, visitor.sourceThreshold,
                (alphaIn || visitor.doInputTransparency) ? visitor.alphaChannels : null, sourceRoi,
                request.isBlend() ? MosaicDescriptor.MOSAIC_TYPE_BLEND : MosaicDescriptor.MOSAIC_TYPE_OVERLAY,
                localHints);

        if (setRoiProperty) {

            //Adding globalRoi to the output
            RenderedOp rop = (RenderedOp) mosaic;
            ROI globalRoi = null;
            ROI[] rois = sourceRoi;
            for (int i = 0; i < rois.length; i++) {
                if (globalRoi == null) {
                    globalRoi = new ROIGeometry(((ROIGeometry) rois[i]).getAsGeometry());
                } else {
                    globalRoi = globalRoi.add(rois[i]);
                }
            }
            rop.setProperty("ROI", globalRoi);
        }

        if (LOGGER.isLoggable(Level.FINE))
            LOGGER.fine("Mosaic created ");

        // create the coverage
        return mosaic;

    }

    /**
     * This method is responsible for creating a coverage from the supplied {@link RenderedImage}.
     * 
     * @param image
     * @return
     * @throws IOException
     */
    private GridCoverage2D prepareCoverage(RenderedImage image) throws IOException {

        // creating bands
        final SampleModel sm = image.getSampleModel();
        final ColorModel cm = image.getColorModel();
        final int numBands = sm.getNumBands();
        final GridSampleDimension[] bands = new GridSampleDimension[numBands];
        Set<String> bandNames = new HashSet<String>();
        // setting bands names.
        for (int i = 0; i < numBands; i++) {
            ColorInterpretation colorInterpretation = null;
            String bandName = null;
            if (cm != null) {
                // === color interpretation
                colorInterpretation = TypeMap.getColorInterpretation(cm, i);
                if (colorInterpretation == null) {
                    throw new IOException("Unrecognized sample dimension type");
                }

                bandName = colorInterpretation.name();
                if (bandNames.contains(bandName)) {// make sure we create no duplicate band names
                    bandName = "Band" + (i + 1);
                }
            } else { // no color model
                bandName = "Band" + (i + 1);
                colorInterpretation = ColorInterpretation.UNDEFINED;
            }

            // sample dimension type
            final SampleDimensionType st = TypeMap.getSampleDimensionType(sm, i);

            // set some no data values, as well as Min and Max values
            final double noData;
            double min = -Double.MAX_VALUE, max = Double.MAX_VALUE;
            if (backgroundValues != null) {
                // sometimes background values are not specified as 1 per each band, therefore we need to be careful
                noData = backgroundValues[backgroundValues.length > i ? i : 0];
            } else {
                if (st.compareTo(SampleDimensionType.REAL_32BITS) == 0)
                    noData = Float.NaN;
                else if (st.compareTo(SampleDimensionType.REAL_64BITS) == 0)
                    noData = Double.NaN;
                else if (st.compareTo(SampleDimensionType.SIGNED_16BITS) == 0) {
                    noData = Short.MIN_VALUE;
                    min = Short.MIN_VALUE;
                    max = Short.MAX_VALUE;
                } else if (st.compareTo(SampleDimensionType.SIGNED_32BITS) == 0) {
                    noData = Integer.MIN_VALUE;

                    min = Integer.MIN_VALUE;
                    max = Integer.MAX_VALUE;
                } else if (st.compareTo(SampleDimensionType.SIGNED_8BITS) == 0) {
                    noData = -128;
                    min = -128;
                    max = 127;
                } else {
                    //unsigned
                    noData = 0;
                    min = 0;

                    // compute max
                    if (st.compareTo(SampleDimensionType.UNSIGNED_1BIT) == 0)
                        max = 1;
                    else if (st.compareTo(SampleDimensionType.UNSIGNED_2BITS) == 0)
                        max = 3;
                    else if (st.compareTo(SampleDimensionType.UNSIGNED_4BITS) == 0)
                        max = 7;
                    else if (st.compareTo(SampleDimensionType.UNSIGNED_8BITS) == 0)
                        max = 255;
                    else if (st.compareTo(SampleDimensionType.UNSIGNED_16BITS) == 0)
                        max = 65535;
                    else if (st.compareTo(SampleDimensionType.UNSIGNED_32BITS) == 0)
                        max = Math.pow(2, 32) - 1;

                }

            }
            bands[i] = new SimplifiedGridSampleDimension(bandName, st, colorInterpretation, noData, min, max, 1, //no scale 
                    0, //no offset
                    null).geophysics(true);
        }

        // creating the final coverage by keeping into account the fact that we
        Map<String, String> properties = null;
        if (granulesPaths != null) {
            properties = new HashMap<String, String>();
            properties.put(AbstractGridCoverage2DReader.FILE_SOURCE_PROPERTY, granulesPaths);
        }

        return coverageFactory.create(rasterManager.getCoverageIdentifier(), image,
                new GridGeometry2D(new GridEnvelope2D(PlanarImage.wrapRenderedImage(image).getBounds()),
                        PixelInCell.CELL_CORNER, finalGridToWorldCorner,
                        this.mosaicBBox.getCoordinateReferenceSystem(), hints),
                bands, null, properties);

    }

}