/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2014-2015, 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.coverage.processing.operation; import it.geosolutions.jaiext.JAIExt; import it.geosolutions.jaiext.range.NoDataContainer; import it.geosolutions.jaiext.range.Range; import it.geosolutions.jaiext.range.RangeFactory; import sun.font.CreatedFontTracker; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.Rectangle2D; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.media.jai.ImageLayout; import javax.media.jai.Interpolation; import javax.media.jai.InterpolationNearest; import javax.media.jai.JAI; import javax.media.jai.ParameterBlockJAI; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.ROIShape; import javax.media.jai.operator.MosaicDescriptor; import javax.media.jai.operator.MosaicType; import org.geotools.coverage.GridSampleDimension; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.InvalidGridGeometryException; import org.geotools.coverage.processing.CoverageProcessingException; import org.geotools.coverage.processing.OperationJAI; import org.geotools.factory.GeoTools; import org.geotools.factory.Hints; import org.geotools.geometry.Envelope2D; import org.geotools.geometry.GeneralEnvelope; import org.geotools.image.ImageWorker; import org.geotools.metadata.iso.citation.Citations; import org.geotools.parameter.DefaultParameterDescriptor; import org.geotools.parameter.ImagingParameterDescriptors; import org.geotools.parameter.ImagingParameters; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.transform.ConcatenatedTransform; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.image.ImageUtilities; import org.geotools.util.Utilities; import it.geosolutions.jaiext.utilities.ImageLayout2; import org.opengis.coverage.ColorInterpretation; import org.opengis.coverage.Coverage; import org.opengis.coverage.grid.GridGeometry; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.parameter.InvalidParameterValueException; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.TransformException; import org.opengis.util.InternationalString; /** * This operation does a mosaic of multiple {@link GridCoverage2D}s. The {@link GridCoverage2D}s can have different resolutions; the operation will * resample them to the same one. The policies for choosing the output resolution are: * <ul> * <li>resolution of the FIRST coverage (Default)</li> * <li>FINE resolution</li> * <li>COARSE resolution</li> * <li>resolution of an EXTERNAL GridGeometry</li> * </ul> * * Note that the operation requires that all the {@link GridCoverage2D}s are in the same CRS, else an exception is thrown. * * The input parameters of the operation are: * <ul> * <li>a Collection of the {@link GridCoverage2D} to mosaic</li> * <li>an optional {@link GridGeometry} object for setting the final resolution and BoundingBox</li> * <li>an optional {@link String} indicating the policy to use for choosing the resolution</li> * <li>an optional {@link double[]} indicating the nodata values to set for the background. Note that the only the first value will be used</li> * </ul> * * @author Nicola Lagomarsini GesoSolutions S.A.S. * */ public class Mosaic extends OperationJAI { private static final int THRESHOLD_PARAM = 3; private static final int BACKGROUND_PARAM = 4; private static final int NODATA_RANGE_PARAM = 5; private static final int MOSAIC_TYPE_PARAM = 0; private static final int ALPHA_PARAM = 1; private static final int ROI_PARAM = 2; /** Name for the COVERAGE_INDEX parameter */ public static final String POLICY = "policy"; /** Name for the GG2D parameter */ public static final String GEOMETRY = "geometry"; /** Name for the Sources parameter */ public static final String SOURCES_NAME = "Sources"; /** Name for the Output No Data parameter */ public static final String OUTNODATA_NAME = "outputNoData"; /** Name for the input Alpha bands*/ public static final String ALPHA_NAME = "alphas"; /** * The parameter descriptor for the Sources. */ public static final ParameterDescriptor SOURCES = new DefaultParameterDescriptor(Citations.JAI, SOURCES_NAME, Collection.class, // Value class (mandatory) null, // Array of valid values null, // Default value null, // Minimal value null, // Maximal value null, // Unit of measure true); /** * The parameter descriptor for the GridGeometry to use. */ public static final ParameterDescriptor<GridGeometry> GG = new DefaultParameterDescriptor<GridGeometry>( Citations.JAI, GEOMETRY, GridGeometry.class, // Value class (mandatory) null, // Array of valid values null, // Default value null, // Minimal value null, // Maximal value null, // Unit of measure false); /** * The parameter descriptor for the GridGeometry choosing policy. */ public static final ParameterDescriptor<String> GEOMETRY_POLICY = new DefaultParameterDescriptor<String>( Citations.JAI, POLICY, String.class, // Value class (mandatory) null, // Array of valid values null, // Default value null, // Minimal value null, // Maximal value null, // Unit of measure false); /** * The parameter descriptor for the Transformation Choice. */ public static final ParameterDescriptor<double[]> OUTPUT_NODATA = new DefaultParameterDescriptor<double[]>( Citations.JAI, OUTNODATA_NAME, double[].class, // Value class (mandatory) null, // Array of valid values null, // Default value null, // Minimal value null, // Maximal value null, // Unit of measure false); /** * The parameter descriptor for the Alpha band. */ public static final ParameterDescriptor<Collection> ALPHA = new DefaultParameterDescriptor<Collection>( Citations.JAI, ALPHA_NAME, Collection.class, // Value class (mandatory) null, // Array of valid values null, // Default value null, // Minimal value null, // Maximal value null, // Unit of measure false); private static Set<ParameterDescriptor> REPLACED_DESCRIPTORS; // Replace the old parameter descriptor group with a new one with the old parameters and the new // ones defined above. static { final Set<ParameterDescriptor> replacedDescriptors = new HashSet<ParameterDescriptor>(); replacedDescriptors.add(SOURCES); replacedDescriptors.add(GG); replacedDescriptors.add(GEOMETRY_POLICY); replacedDescriptors.add(OUTPUT_NODATA); replacedDescriptors.add(ALPHA); REPLACED_DESCRIPTORS = Collections.unmodifiableSet(replacedDescriptors); } /** * Enum used for choosing the output {@link GridGeometry2D} to use and then resampling all the {@link GridCoverage2D} to its resolution. * * @author Nicola Lagomarsini GesoSolutions S.A.S. * */ public enum GridGeometryPolicy { FIRST("first") { @Override public ResampledRasters resampleGridGeometry(GridCoverage2D[] sources, GridCoverage2D[] alphas, GridGeometry2D external, ParameterValueGroup parameters, Hints hints) { // Index associated to the first coverage int index = PRIMARY_SOURCE_INDEX; // Selection of the first GridGeometry2D object to use GridGeometry2D finalGG = extractFinalGridGeometry(sources, index); // GridCoverage resampling return resampleCoverages(sources, alphas, finalGG, parameters, hints); } }, FINE("fine") { @Override public ResampledRasters resampleGridGeometry(GridCoverage2D[] sources, GridCoverage2D[] alphas, GridGeometry2D external, ParameterValueGroup parameters, Hints hints) { // Number of the sources to use int numSources = sources.length; // Selection of the first GridGeometry GridGeometry2D grid = sources[0].getGridGeometry(); Envelope2D env = grid.getEnvelope2D(); GridEnvelope2D gridEnv = grid.getGridRange2D(); // Method for searching the index at the highest resolution. Suppose that the coverages contains the same // resolution on both axis double res = env.width / gridEnv.width; // Coverage index int index = PRIMARY_SOURCE_INDEX; // Search for the minimum value of the resolution for (int i = 1; i < numSources; i++) { GridGeometry2D gridI = sources[i].getGridGeometry(); Envelope2D envI = gridI.getEnvelope2D(); GridEnvelope2D gridEnvI = gridI.getGridRange2D(); double resValue = envI.width / gridEnvI.width; // Search the index associated to the better resolution if (resValue < res) { res = resValue; index = i; } } // Calculation of the final GridGeometry to use GridGeometry2D finalGG = extractFinalGridGeometry(sources, index); // Coverage resampling return resampleCoverages(sources, alphas, finalGG, parameters, hints); } }, COARSE("coarse") { @Override public ResampledRasters resampleGridGeometry(GridCoverage2D[] sources, GridCoverage2D[] alphas, GridGeometry2D external, ParameterValueGroup parameters, Hints hints) { // Number of the sources to use int numSources = sources.length; // Selection of the first GridGeometry GridGeometry2D grid = sources[0].getGridGeometry(); Envelope2D env = grid.getEnvelope2D(); GridEnvelope2D gridEnv = grid.getGridRange2D(); // Method for searching the index at the lowest resolution. Suppose that the coverages contains the same // resolution on both axis double res = env.width / gridEnv.width; // Coverage index int index = PRIMARY_SOURCE_INDEX; // Search for the minimum value of the resolution for (int i = 1; i < numSources; i++) { GridGeometry2D gridI = sources[i].getGridGeometry(); Envelope2D envI = gridI.getEnvelope2D(); GridEnvelope2D gridEnvI = gridI.getGridRange2D(); double resValue = envI.width / gridEnvI.width; // Search the index associated to the worst resolution if (resValue > res) { res = resValue; index = i; } } // Calculation of the final GridGeometry to use GridGeometry2D finalGG = extractFinalGridGeometry(sources, index); // Coverage resampling return resampleCoverages(sources, alphas, finalGG, parameters, hints); } }, EXTERNAL("external") { @Override public ResampledRasters resampleGridGeometry(GridCoverage2D[] sources, GridCoverage2D[] alphas, GridGeometry2D external, ParameterValueGroup parameters, Hints hints) { // Check if the external GridGeometry is present if (external == null) { throw new CoverageProcessingException("No input GridGeometry found"); } // Coverage resampling return resampleCoverages(sources, alphas, external, parameters, hints); } }; /** Name associated to the {@link GridGeometryPolicy} object */ private String name; private GridGeometryPolicy(String name) { this.name = name; } /** * Method for resampling the input {@link GridCoverage2D} objects. The output of the method is an object containing the resampled * {@link RenderedImage}s and the final {@link GridGeometry2D} object to use. * * @param sources * @param alphas * @param external * @param parameters * @param hints * @return */ public abstract ResampledRasters resampleGridGeometry(GridCoverage2D[] sources, GridCoverage2D[] alphas, GridGeometry2D external, ParameterValueGroup parameters, Hints hints); /** * Static method to use for choosing the {@link GridGeometryPolicy} object associated to the input string. * * @param policyString * @return */ public static GridGeometryPolicy getPolicyFromString(String policyString) { if (policyString.equalsIgnoreCase(FIRST.name)) { return FIRST; } else if (policyString.equalsIgnoreCase(FINE.name)) { return FINE; } else if (policyString.equalsIgnoreCase(COARSE.name)) { return COARSE; } else if (policyString.equalsIgnoreCase(EXTERNAL.name)) { return EXTERNAL; } return null; } /** * Private method for resampling the {@link GridCoverage2D}s to the same resolution imposed by the {@link GridGeometry2D} object. * * @param sources * @param alphas * @param external * @param parameters * @return */ private static ResampledRasters resampleCoverages(GridCoverage2D[] sources, GridCoverage2D[] alphas, GridGeometry2D external, ParameterValueGroup parameters, Hints hints) { // Number of the sources to use int numSources = sources.length; // Creation of an array of the RenderedImages to use RenderedImage[] rasters = new RenderedImage[numSources]; // Creation of an array of background values double[] backgrounds = new double[numSources]; // Creation of an array of rois ROI[] rois = new ROI[numSources]; // Creation of an array of NoData boolean hasNoDataProp = false; // Selection of the GridToWorld transformation associated to the External GG2D MathTransform g2w = external.getGridToCRS2D(PixelOrientation.UPPER_LEFT); // Initial null value for NoData double[] nodata = null; // Check if the output nodata value is set as parameter Object outputNodata = parameters.parameter(OUTNODATA_NAME).getValue(); if (outputNodata != null && outputNodata instanceof double[]) { nodata = ((double[]) outputNodata); } // Checking if the external alpha bands are defined boolean hasAlpha = alphas != null && alphas.length > 0; PlanarImage[] alphaArray = new PlanarImage[numSources]; // Cycle around the various sources for (int i = 0; i < numSources; i++) { // For each source, create a new GridGeometry which at the same resolution of the imposed one GridCoverage2D coverage = sources[i]; GridGeometry2D inputGG = coverage.getGridGeometry(); // Check if the transform from one gridGeometry to the other is an Identity transformation MathTransform g2wS = inputGG.getGridToCRS2D(PixelOrientation.UPPER_LEFT); MathTransform w2gD = external.getCRSToGrid2D(PixelOrientation.UPPER_LEFT); // Creation of a Concatenated transformation in order to check if the final transformation from // source space to the final space is an identity. MathTransform concatenated = ConcatenatedTransform.create(g2wS, w2gD); // No operation must be done if the transformation is an Identity if (concatenated != null && concatenated.isIdentity()) { RenderedImage renderedImage = coverage.getRenderedImage(); rasters[i] = renderedImage; // Get ROI from the coverage rois[i] = CoverageUtilities.getROIProperty(coverage); // Add the alpha band if (hasAlpha && alphas[i] != null) { checkAlpha(coverage, alphas[i]); alphaArray[i] = PlanarImage.wrapRenderedImage(alphas[i].getRenderedImage()); // Mask alpha band with ROI in order to see ROI during Mosaic operation if (rois[i] != null) { ImageWorker w = new ImageWorker(alphaArray[i]); // Expand ROI Image to alpha size ImageWorker w1 = new ImageWorker(rois[i].getAsImage()); ImageLayout layout = new ImageLayout(); layout.setMinX(alphaArray[i].getMinX()); layout.setMinY(alphaArray[i].getMinY()); layout.setWidth(alphaArray[i].getWidth()); layout.setHeight(alphaArray[i].getHeight()); w1.setRenderingHint(JAI.KEY_IMAGE_LAYOUT, layout); w1.translate(0f, 0f, null); // Mask Alpha w.mask(w1.getRenderedImage(), false, 0); } } double fillValue = CoverageUtilities.getBackgroundValues(coverage)[0]; backgrounds[i] = fillValue; // Get NoData as property if present NoDataContainer noDataProperty = CoverageUtilities.getNoDataProperty(coverage); hasNoDataProp |= noDataProperty != null; } else { // New GridGeometry GridGeometry2D newGG = new GridGeometry2D(PixelInCell.CELL_CORNER, g2w, inputGG.getEnvelope(), GeoTools.getDefaultHints()); try { // Transformation of the input envelope in the Raster Space GeneralEnvelope transformed = CRS.transform( g2w.inverse(), inputGG.getEnvelope()); // Rounding of the bounds Rectangle rect = transformed.toRectangle2D().getBounds(); // Creation of a new GridEnvelope to set for the new GridGeometry GridEnvelope2D gEnv2 = new GridEnvelope2D(rect); // Creation of the new GridGeometry newGG = new GridGeometry2D(gEnv2, inputGG.getEnvelope()); } catch (InvalidGridGeometryException e) { throw new CoverageProcessingException(e); } catch (NoninvertibleTransformException e) { throw new CoverageProcessingException(e); } catch (TransformException e) { throw new CoverageProcessingException(e); } // Initialization of the nodata value double[] fillValue = null; // Selection of the nodata value if (nodata == null) { fillValue = CoverageUtilities.getBackgroundValues(coverage); } else { fillValue = nodata; } // Resample to the new resolution rasters[i] = GridCoverage2DRIA.create(coverage, newGG, fillValue, hints, CoverageUtilities.getROIProperty(coverage)); // Resample also the alpha band if (hasAlpha && alphas[i] != null) { checkAlpha(coverage, alphas[i]); RenderedImage al = GridCoverage2DRIA.create(alphas[i], newGG, new double[1], hints, CoverageUtilities.getROIProperty(coverage)); alphaArray[i] = PlanarImage.wrapRenderedImage(al); } backgrounds[i] = fillValue[0]; // Resample to the new resolution GridCoverage2DRIA.GridCoverage2DRIAPropertyGenerator propertyGenerator = new GridCoverage2DRIA.GridCoverage2DRIAPropertyGenerator(); Object property = propertyGenerator.getProperty("roi", rasters[i]); ROI roi = (property != null && property instanceof ROI ) ? (ROI) property : null; rois[i] = roi; // Get NoData as property if present NoDataContainer noDataProperty = CoverageUtilities.getNoDataProperty(coverage); hasNoDataProp |= noDataProperty != null; } } // Create the final object containing the final GridGeometry and the resampled RenderedImages ResampledRasters rr = new ResampledRasters(); rr.setFinalGeometry(external); rr.setRasters(rasters); rr.setAlphas(alphaArray); rr.setBackgrounds(backgrounds); rr.setRois(rois); rr.setHasNoData(hasNoDataProp); return rr; } /** * Method for checking if Alpha Coverage and Image Coverage have the same dimensions * * @param coverage * @param alpha */ private static void checkAlpha(GridCoverage2D coverage, GridCoverage2D alpha) { // Check GridGeometries if (!coverage.getGridGeometry().equals(alpha.getGridGeometry())) { throw new CoverageProcessingException( "Alpha Coverage and Source Coverage does not have the same dimensions"); } } /** * This method creates a new {@link GridGeometry2D} object based on that of the {@link GridCoverage2D} defined by the index. * * @param sources * @param index * @return */ private static GridGeometry2D extractFinalGridGeometry(GridCoverage2D[] sources, int index) { // Select the GridGeometry of the first coverage GridGeometry2D gg = sources[index].getGridGeometry(); MathTransform g2w = gg.getGridToCRS2D(PixelOrientation.UPPER_LEFT); // Initial Bounding box Envelope2D bbox = gg.getEnvelope2D(); // Number of the sources to use int numSources = sources.length; // Cycle on all the GridCoverages in order to create the final Bounding box for (int i = 0; i < numSources; i++) { bbox.include(sources[i].getEnvelope2D()); } // Creation of a final GridGeometry containing the final Bounding Box GridGeometry2D finalGG = new GridGeometry2D(PixelInCell.CELL_CORNER, g2w, bbox, GeoTools.getDefaultHints()); return finalGG; } } public Mosaic() { super(getOperationDescriptor("Mosaic"), new ImagingParameterDescriptors( getOperationDescriptor("Mosaic"), REPLACED_DESCRIPTORS)); } public Coverage doOperation(final ParameterValueGroup parameters, final Hints hints) throws CoverageProcessingException { /* * Extracts the source grid coverages now as a List. The sources will be set in the ParameterBlockJAI (as RenderedImages) later. */ final Collection<GridCoverage2D> sourceCollection = new ArrayList<GridCoverage2D>(); extractSources(parameters, sourceCollection, null); // Selection of the source number int numSources = sourceCollection.size(); GridCoverage2D[] sources = new GridCoverage2D[numSources]; // Creation of an array of GridCoverage2D from the input collection sourceCollection.toArray(sources); // Selection of the CRS of the first coverage in order to check that the CRS is the same for all the GridCoverages GridCoverage2D firstCoverage = sources[PRIMARY_SOURCE_INDEX]; CoordinateReferenceSystem crs = firstCoverage.getCoordinateReferenceSystem(); for (int i = 0; i < sources.length; i++) { final GridCoverage2D source = sources[i]; CoordinateReferenceSystem crsSource = source.getCoordinateReferenceSystem(); if (!CRS.equalsIgnoreMetadata(crs, crsSource)) { throw new CoverageProcessingException("Input Coverages have different CRS"); } } // Preparation of the input parameters and resampling of the source images final Params params = prepareParameters(parameters, sources, hints); /* * Applies the operation. This delegates the work to the chain of 'deriveXXX' methods. */ return deriveGridCoverage(sources, params); } /** * Prepares the parameters to store in the {@link ParameterBlockJAI} object and resample the input {@link GridCoverage2D}. * * @param parameters * @param sources * @param hints * @return */ private Params prepareParameters(final ParameterValueGroup parameters, GridCoverage2D[] sources, Hints hints) { final ImagingParameters copy = (ImagingParameters) descriptor.createValue(); final ParameterBlockJAI block = (ParameterBlockJAI) copy.parameters; // Object indicating the policy to use for resampling all the GridCoverages to the same GridGeometry GridGeometryPolicy policy = null; // Check if the External GridGeometry is present Object externalGG = parameters.parameter(GEOMETRY).getValue(); GridGeometry2D gg = null; if (externalGG != null && externalGG instanceof GridGeometry2D) { gg = (GridGeometry2D) externalGG; policy = GridGeometryPolicy.EXTERNAL; } else { // Check if the GridGeometry selection policy is present Object ggPolicy = parameters.parameter(POLICY).getValue(); if (ggPolicy != null && ggPolicy instanceof String) { policy = GridGeometryPolicy.getPolicyFromString((String) ggPolicy); } } // No policy defined, the first GridCoverage is used. if (policy == null) { policy = GridGeometryPolicy.FIRST; } // Getting Alpha bands if present Object alphaBandList = parameters.parameter(ALPHA_NAME).getValue(); GridCoverage2D[] alphaCovs = null; if (alphaBandList != null && alphaBandList instanceof Collection) { Collection<GridCoverage2D> alphas = (Collection<GridCoverage2D>) alphaBandList; alphaCovs = new GridCoverage2D[alphas.size()]; alphas.toArray(alphaCovs); } // Resample to the defined GridGeometry ResampledRasters rr = policy.resampleGridGeometry(sources, alphaCovs, gg, parameters, hints); // Get the resampled RenderedImages RenderedImage[] rasters = rr.getRasters(); // Get returnedROIs ROI[] newRois = rr.getRois(); // Setting of the final GridGeometry GridGeometry2D finalGeometry = rr.getFinalGeometry(); if (finalGeometry == null) { throw new CoverageProcessingException("No final GridGeometry found"); } int numSources = rasters.length; // Setting the source rasters for the mosaic if(Boolean.TRUE.equals(hints.get(JAI.KEY_REPLACE_INDEX_COLOR_MODEL))) { // the mosaic operation will blow up in this case, internally the Raster accessors // are not getting configured to do expansion as needed. Work around it. for (int i = 0; i < numSources; i++) { RenderedImage source = rasters[i]; if(source.getColorModel() instanceof IndexColorModel) { source = new ImageWorker(source).forceComponentColorModel().getRenderedImage(); } block.setSource(source, i); } hints = new Hints(hints); hints.add(new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE)); hints.add(new RenderingHints(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.TRUE)); } else { for (int i = 0; i < numSources; i++) { block.setSource(rasters[i], i); } } // Setting the nodata values for the areas not covered by any GridCoverage. double[] nodata = null; // Check if the output nodata value is present Object outputNodata = parameters.parameter(OUTNODATA_NAME).getValue(); if (outputNodata != null && outputNodata instanceof double[]) { nodata = ((double[]) outputNodata); } else { nodata = CoverageUtilities.getBackgroundValues(sources[PRIMARY_SOURCE_INDEX]); } // Setting of the output nodata block.set(nodata, BACKGROUND_PARAM); // Setting of the ROI associated to each GridCoverage // We need to add its roi in order to avoid problems with the mosaics sources overlapping ROI[] rois = new ROI[numSources]; // Cycle on each coverage in order to add the associated ROI for (int i = 0; i < numSources; i++) { if(newRois != null && newRois[i] != null){ rois[i] = newRois[i]; }else{ rois[i] = new ROIShape(PlanarImage.wrapRenderedImage(rasters[i]).getBounds()); } } block.set(rois, ROI_PARAM); // If at least one image contains Alpha channel, it is used for the mosaic if (rr.getAlphas() != null) { block.set(rr.getAlphas(), ALPHA_PARAM); } // Setting of the Threshold to use double threshold = CoverageUtilities.getMosaicThreshold(rasters[PRIMARY_SOURCE_INDEX] .getSampleModel().getDataType()); // Setting of the Threshold object to use for the mosaic block.set(new double[][] { { threshold } }, THRESHOLD_PARAM); // Setting of the Mosaic type as Overlay block.set(MosaicDescriptor.MOSAIC_TYPE_OVERLAY, MOSAIC_TYPE_PARAM); // Check if it is a JAI-Ext operation if(JAIExt.isJAIExtOperation("Mosaic")){ // Get the nodata values double[] nodatas = rr.getBackgrounds(); if(nodatas != null && rr.hasNoData()){ Range[] ranges = new Range[numSources]; for(int i = 0; i < numSources; i++){ double value = nodatas[i]; ranges[i] = RangeFactory.create(value, value); } block.set(ranges, NODATA_RANGE_PARAM); } } // Creation of the finel Parameters Params params = new Params(block, hints, finalGeometry); params.rr = rr; return params; } /** * Applies a JAI operation to the coverages. This method is invoked by {@link #doOperation}. This implementation performs the following steps: * * <ul> * <li>Applied the JAI operation using {@link #createRenderedImage}.</li> * <li>Wraps the result in a {@link GridCoverage2D} object.</li> * </ul> * * @param sources The source coverages. * @param parameters Parameters, rendering hints and coordinate reference system to use. * @return The result as a grid coverage. * * @see #doOperation * @see JAI#createNS */ private GridCoverage2D deriveGridCoverage(final GridCoverage2D[] sources, final Params parameters) { GridCoverage2D primarySource = sources[PRIMARY_SOURCE_INDEX]; /* * Set the rendering hints image layout. Only the following properties will be set: * * - Width - Height */ RenderingHints hintsStart = ImageUtilities.getRenderingHints(parameters.getSource()); RenderingHints hints = null; // Addition of the Hints if (parameters.hints != null) { if (hintsStart != null) { hints = new Hints(hintsStart); hints.add(parameters.hints); // May overwrite the image layout we have just set. } else { hints = new Hints(parameters.hints); } } // Layout associated to the input RenderingHints ImageLayout layoutOld = (hints != null) ? (ImageLayout) hints.get(JAI.KEY_IMAGE_LAYOUT) : null; ImageLayout layout = null; // Check on the ImageLayout if (layoutOld != null) { layout = (ImageLayout) layoutOld.clone(); // Unset the previous dimension parameters layout.unsetValid(ImageLayout.MIN_X_MASK); layout.unsetValid(ImageLayout.MIN_Y_MASK); layout.unsetValid(ImageLayout.WIDTH_MASK); layout.unsetValid(ImageLayout.HEIGHT_MASK); // there might be color expansion layout.unsetValid(ImageLayout.COLOR_MODEL_MASK); layout.unsetValid(ImageLayout.SAMPLE_MODEL_MASK); } else { // Create a new one layout = new ImageLayout2(); } // Get the GridRange associated to the final GridGeometry to use GridEnvelope2D gridRange = parameters.finalGeometry.getGridRange2D(); // Then set the parameters associated to the final GridGeometry used layout.setMinX(gridRange.x); layout.setMinY(gridRange.y); layout.setWidth(gridRange.width); layout.setHeight(gridRange.height); // Set the new layout for the rendering hints if (hints == null) { hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout); } else { hints.put(JAI.KEY_IMAGE_LAYOUT, layout); } /* * Performs the operation using JAI and construct the new grid coverage. Uses the coordinate system from the main source coverage in order to * preserve the extra dimensions (if any). The first two dimensions should be equal to the coordinate system set in the 'parameters' block. */ final InternationalString name = deriveName(sources, -1, null); final CoordinateReferenceSystem crs = primarySource.getCoordinateReferenceSystem(); final MathTransform toCRS = parameters.finalGeometry.getGridToCRS(); final RenderedImage data = createRenderedImage(parameters.parameters, hints); final Map<String, ?> properties = getProperties(data, crs, name, toCRS, sources, parameters); return getFactory(parameters.hints).create(name, // The grid coverage name data, // The underlying data crs, // The coordinate system (may not be 2D). toCRS, // The grid transform (may not be 2D). getOutputSampleDimensions(primarySource.getSampleDimensions(), data), // The sample dimensions sources, // The source grid coverages. properties); // Properties } /** * We override this one to get some extra behavior that ImageWorker has (ROI, paletted images management) */ protected RenderedImage createRenderedImage(final ParameterBlockJAI parameters, final RenderingHints hints) { parameters.getSources(); RenderedImage[] images = (RenderedImage[]) parameters.getSources() .toArray(new RenderedImage[parameters.getSources().size()]); MosaicType type = getParameter(parameters, 0); PlanarImage[] alphas = getParameter(parameters, ALPHA_PARAM); ROI[] rois = getParameter(parameters, ROI_PARAM); double[][] thresholds = getParameter(parameters, THRESHOLD_PARAM); Range[] noData = getParameter(parameters, NODATA_RANGE_PARAM); double[] backgrounds = getParameter(parameters, BACKGROUND_PARAM); ImageWorker iw = new ImageWorker(); iw.setRenderingHints(hints); iw.setBackground(backgrounds); iw.mosaic(images, type, alphas, rois, thresholds, noData); return iw.getRenderedImage(); } private <T> T getParameter(ParameterBlockJAI pb, int index) { if(pb.getNumParameters() > index) { return (T) pb.getObjectParameter(index); } else { return null; } } private GridSampleDimension[] getOutputSampleDimensions(GridSampleDimension[] sampleDimensions, RenderedImage data) { // if there was no color model expansion, we are fine int outputNumBands = data.getSampleModel().getNumBands(); if(outputNumBands == sampleDimensions.length) { return sampleDimensions; } GridSampleDimension[] newSampleDimensions = new GridSampleDimension[outputNumBands]; // GridSampleDimension reference = sampleDimensions[0]; // ColorInterpretation[] interpretations = new ColorInterpretation[outputNumBands]; // switch (outputNumBands) { // case 1: // interpretations[0] = ColorInterpretation.GRAY_INDEX; // break; // case 2: // interpretations[0] = ColorInterpretation.GRAY_INDEX; // interpretations[1] = ColorInterpretation.ALPHA_BAND; // break; // // case 3: // interpretations[0] = ColorInterpretation.RED_BAND; // interpretations[1] = ColorInterpretation.GREEN_BAND; // interpretations[2] = ColorInterpretation.BLUE_BAND; // break; // // case 4: // interpretations[0] = ColorInterpretation.RED_BAND; // interpretations[1] = ColorInterpretation.GREEN_BAND; // interpretations[2] = ColorInterpretation.BLUE_BAND; // interpretations[3] = ColorInterpretation.ALPHA_BAND; // break; // // default: // for (int i = 0; i < outputNumBands; i++) { // interpretations[i] = ColorInterpretation.UNDEFINED; // } // break; // } for (int i = 0; i < newSampleDimensions.length; i++) { newSampleDimensions[i] = new GridSampleDimension("Band" + i); } return newSampleDimensions; } protected Map<String, ?> getProperties(RenderedImage data, CoordinateReferenceSystem crs, InternationalString name, MathTransform gridToCRS, GridCoverage2D[] sources, Params parameters) { Map properties; if (sources[0].getProperties() == null) { properties = new HashMap<>(); } else { properties = new HashMap<>(sources[0].getProperties()); } // Get the ROI and NoData property from the parameterBlock ParameterBlockJAI jai = parameters.parameters; int numSources = jai.getNumSources(); // ROI // Object roiParam = jai.getObjectParameter(2); ResampledRasters rr = parameters.rr; if (rr != null && rr.getRois() != null) { ROI[] rois = rr.getRois(); ROI finalROI = null; for (int i = 0; i < numSources; i++) { if (finalROI == null) { finalROI = rois[i]; } else if(rois[i] == null) { // no ROI, the image is full RenderedImage ri = sources[i].getRenderedImage(); finalROI.add(new ROIShape(new Rectangle2D.Double(ri.getMinX(), ri.getMinY(), ri.getWidth(), ri.getHeight()))); } else { finalROI.add(rois[i]); } } CoverageUtilities.setROIProperty(properties, finalROI); } // NoData Object nodataParam = jai.getObjectParameter(4); if (nodataParam != null && rr != null && rr.hasNoData()) { CoverageUtilities.setNoDataProperty(properties, nodataParam); } return properties; } protected void extractSources(final ParameterValueGroup parameters, final Collection<GridCoverage2D> sources, final String[] sourceNames) throws ParameterNotFoundException, InvalidParameterValueException { Utilities.ensureNonNull("parameters", parameters); Utilities.ensureNonNull("sources", sources); // Extraction of the sources from the parameters Object srcCoverages = parameters.parameter("Sources").getValue(); if (!(srcCoverages instanceof Collection) || ((Collection) srcCoverages).isEmpty() || !(((Collection) srcCoverages).iterator().next() instanceof GridCoverage2D)) { throw new InvalidParameterValueException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$1, "sources"), "sources", srcCoverages); } // Collection of the sources to use Collection<GridCoverage2D> sourceCoverages = (Collection<GridCoverage2D>) srcCoverages; sources.addAll(sourceCoverages); } /** * A block of parameters for a {@link GridCoverage2D} processed by the {@link Mosaic} operation. * * @author Nicola Lagomarsini */ protected static final class Params { public ResampledRasters rr; /** * The parameters to be given to the {@link JAI#createNS} method. */ public final ParameterBlockJAI parameters; /** * The {@link GridGeometry2D} object to use for the final {@link GridCoverage2D}. */ public final GridGeometry2D finalGeometry; /** * The rendering hints to be given to the {@link JAI#createNS} method. The {@link JAI} instance to use for the {@code createNS} call will be * fetch from the {@link Hints#JAI_INSTANCE} key. */ public final Hints hints; /** * Constructs a new instance of this class with the specified values. */ Params(final ParameterBlockJAI parameters, final Hints hints, final GridGeometry2D finalGeometry) { this.parameters = parameters; this.hints = hints; this.finalGeometry = finalGeometry; } /** * Returns the first source image, or {@code null} if none. */ final RenderedImage getSource() { final int n = parameters.getNumSources(); for (int i = 0; i < n; i++) { final Object source = parameters.getSource(i); if (source instanceof RenderedImage) { return (RenderedImage) source; } } return null; } } /** * A container class used for passing the resampled {@link RenderedImage}s and the final {@link GridGeometry2D} created by the * {@link GridGeometryPolicy}.resampleGridGeometry() method. * * @author Nicola Lagomarsini */ private static final class ResampledRasters { /** * @return The array of the resampled RenderedImages */ public RenderedImage[] getRasters() { return rasters; } /** * Set the array of the resampled RenderedImages * * @param rasters */ public void setRasters(RenderedImage[] rasters) { this.rasters = rasters; } /** * @return The {@link GridGeometry2D} object to use for the mosaic */ public GridGeometry2D getFinalGeometry() { return finalGeometry; } /** * Set the {@link GridGeometry2D} object to use for the mosaic * * @param finalGeometry */ public void setFinalGeometry(GridGeometry2D finalGeometry) { this.finalGeometry = finalGeometry; } /** * @return The {@link PlanarImage} array to use for the mosaic */ public PlanarImage[] getAlphas() { return alphas; } /** * Sets the array of the external alpha bands * * @param rasters */ public void setAlphas(PlanarImage[] alphas) { this.alphas = alphas; } /** * @return The nodata to use for the mosaic */ public double[] getBackgrounds() { return nodata; } /** * Set the nodata values for each mosaic element * * @param nodata */ public void setBackgrounds(double[] nodata) { this.nodata = nodata; } /** * @return The roi to use for the mosaic */ public ROI[] getRois() { return rois; } /** * Set the roi values for each mosaic element * * @param rois */ public void setRois(ROI[] rois) { this.rois = rois; } /** * @return boolean indicating that at least one coverage contains nodata */ public boolean hasNoData() { return hasNoData; } /** * Set the hasNoData boolean parameter * * @param hasNoData */ public void setHasNoData(boolean hasNoData) { this.hasNoData = hasNoData; } /** * The array of the resampled RenderedImages */ private RenderedImage[] rasters; /** * The {@link GridGeometry2D} object to use for the mosaic */ private GridGeometry2D finalGeometry; /** * The {@link PlanarImage} array used for representing external alpha bands */ private PlanarImage[] alphas; /** * Array of the final NoData values */ private double[] nodata; /** * Array of the final roi values */ private ROI[] rois; /** * Boolean indicating if input nodata values are present */ private boolean hasNoData; } }