/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2001-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.coverage.processing; import java.util.List; import java.util.Map; import java.util.Arrays; import java.util.Objects; import java.util.Locale; import java.util.Collections; import java.io.Serializable; import java.awt.RenderingHints; import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import javax.measure.Unit; import javax.media.jai.JAI; import javax.media.jai.ImageLayout; import javax.media.jai.ParameterBlockJAI; import javax.media.jai.OperationRegistry; import javax.media.jai.OperationDescriptor; import javax.media.jai.registry.RenderedRegistryMode; import org.opengis.coverage.Coverage; import org.opengis.coverage.processing.OperationNotFoundException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.MathTransformFactory; import org.opengis.referencing.operation.TransformException; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterValueGroup; import org.opengis.util.InternationalString; import org.opengis.util.FactoryException; import org.geotoolkit.factory.Hints; import org.geotoolkit.factory.FactoryFinder; import org.geotoolkit.coverage.Category; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.coverage.grid.ViewType; import org.geotoolkit.coverage.grid.GridCoverage2D; import org.geotoolkit.coverage.grid.GridGeometry2D; import org.geotoolkit.coverage.grid.InvalidGridGeometryException; import org.geotoolkit.coverage.parameter.ImagingParameters; import org.geotoolkit.coverage.parameter.ImagingParameterDescriptors; import org.geotoolkit.referencing.operation.transform.DimensionFilter; import org.geotoolkit.image.jai.Registry; import org.geotoolkit.internal.referencing.CRSUtilities; import org.geotoolkit.internal.coverage.CoverageUtilities; import org.apache.sis.util.ArraysExt; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.iso.AbstractInternationalString; import org.geotoolkit.resources.Errors; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; import org.geotoolkit.image.internal.ImageUtilities; import org.apache.sis.util.Utilities; /** * * Wraps a JAI's {@link OperationDescriptor} for inter-operability with * <A HREF="http://java.sun.com/products/java-media/jai/">Java Advanced Imaging</A>. * This class help to leverage the rich set of JAI operators in an GeoAPI framework. * {@code OperationJAI} inherits operation name and argument types from {@link OperationDescriptor}, * except the source argument type (usually <code>{@linkplain RenderedImage}.class</code>) which is * set to <code>{@linkplain GridCoverage2D}.class</code>. If there is only one source argument, it * will be renamed {@code "Source"} for better compliance with OpenGIS usage. * <p> * The entry point for applying an operation is the usual {@link #doOperation doOperation} method. * The default implementation forward the call to other methods for different bits of tasks, * resulting in the following chain of calls: * <p> * <blockquote><table> * <tr><td>{@link #doOperation doOperation}: </td> * <td>the entry point.</td></tr> * <tr><td>{@link #resampleToCommonGeometry resampleToCommonGeometry}: </td> * <td>reprojects all sources to the same coordinate reference system.</td></tr> * <tr><td>{@link #deriveGridCoverage deriveGridCoverage}: </td> * <td>gets the destination properties.</td></tr> * <tr><td>{@link #deriveSampleDimension deriveSampleDimension}: </td> * <td>gets the destination sample dimensions.</td></tr> * <tr><td>{@link #deriveCategory deriveCategory}: </td> * <td>gets the destination categories.</td></tr> * <tr><td>{@link #deriveRange deriveRange}: </td> * <td>gets the expected range of values.</td></tr> * <tr><td>{@link #deriveUnit deriveUnit}: </td> * <td>gets the destination units.</td></tr> * <tr><td>{@link #createRenderedImage createRenderedImage}: </td> * <td>the actual call to {@link JAI#createNS JAI.createNS}.</td></tr> * </table></blockquote> * * @author Martin Desruisseaux (IRD, Geomatys) * @author Simone Giannecchini (Geosolutions) * @version 3.00 * * @since 1.2 * @module * * @deprecated The API of this class will change in a future Geotk release. Do not rely on it. */ @Deprecated public class OperationJAI extends Operation2D { /** * Serial number for inter-operability with different versions. */ private static final long serialVersionUID = -5974520239347639965L; /** * The rendered mode for JAI operation, which is {@value}. * * @see RenderedRegistryMode#MODE_NAME */ protected static final String RENDERED_MODE = RenderedRegistryMode.MODE_NAME; /** * The JAI's operation descriptor. */ protected final OperationDescriptor operation; /** * Constructs a grid coverage operation from a JAI operation name. This convenience * constructor fetches the {@link OperationDescriptor} from the specified operation * name using the default {@link JAI} instance. * * @param operation JAI operation name (e.g. {@code "GradientMagnitude"}). * @throws OperationNotFoundException if no JAI descriptor was found for the given name. */ public OperationJAI(final String operation) throws OperationNotFoundException { this(getOperationDescriptor(operation)); } /** * Constructs a grid coverage operation backed by a JAI operation. The operation descriptor * must support the {@value #RENDERED_MODE} mode (which is the case for most JAI operations). * * @param operation The JAI operation descriptor. */ public OperationJAI(final OperationDescriptor operation) { this(operation, new ImagingParameterDescriptors(operation)); } /** * Constructs a grid coverage operation backed by a JAI operation. The operation descriptor * must supports the {@value #RENDERED_MODE} mode (which is the case for most JAI operations). * * @param operation The JAI operation descriptor. * @param descriptor The OGC parameters descriptor. */ protected OperationJAI(final OperationDescriptor operation, final ParameterDescriptorGroup descriptor) { super(descriptor); this.operation = operation; ensureNonNull("operation", operation); ensureRenderedImage(operation.getDestClass(RENDERED_MODE)); final Class<?>[] sourceClasses = operation.getSourceClasses(RENDERED_MODE); if (sourceClasses != null) { final int length = sourceClasses.length; assert length == operation.getNumSources(); for (int i=0; i<length; i++) { ensureRenderedImage(sourceClasses[i]); } } assert super.getNumSources() == operation.getNumSources(); } /** * Returns the operation descriptor for the specified JAI operation name. This method * uses the default {@link JAI} instance and looks for the {@value #RENDERED_MODE} mode. * * @param name The operation name. * @return The operation descriptor for the given name. * @throws OperationNotFoundException if no JAI descriptor was found for the given name. * * @since 2.4 */ protected static OperationDescriptor getOperationDescriptor(final String name) throws OperationNotFoundException { final OperationRegistry registry = JAI.getDefaultInstance().getOperationRegistry(); OperationDescriptor operation = (OperationDescriptor) registry.getDescriptor(RENDERED_MODE, name); if (operation != null) { return operation; } if (name.startsWith("org.geotoolkit.") && registry.getDescriptor(RENDERED_MODE, "org.geotoolkit.Combine") == null) { try { // Try to register our operations. Registry.registerGeotoolkitServices(registry); } catch (RuntimeException e) { Logging.unexpectedException(AbstractCoverageProcessor.LOGGER, OperationJAI.class, "getOperationDescriptor", e); } // Try to get it again. operation = (OperationDescriptor) registry.getDescriptor(RENDERED_MODE, name); if (operation != null) { return operation; } } throw new OperationNotFoundException(Errors.format(Errors.Keys.NoSuchOperation_1, name)); } /** * Ensures that the specified class is assignable to {@link RenderedImage}. */ private static void ensureRenderedImage(final Class<?> classe) throws IllegalArgumentException { if (!RenderedImage.class.isAssignableFrom(classe)) { throw new IllegalArgumentException(Errors.format( Errors.Keys.IllegalClass_2, classe, RenderedImage.class)); } } /** * Copies parameter values from the specified {@link ParameterValueGroup} to the * {@link ParameterBlockJAI}, except the sources. * * {@note It would be possible to use <code>ImagingParameters.parameters(...)</code> directly * in some occasions. However, we perform an unconditional copy instead because some * operations (e.g. <code>"GradientMagnitude"</code>) may change the values.} * * @param parameters The {@link ParameterValueGroup} to be copied. * @return A copy of the provided {@link ParameterValueGroup} as a JAI block. * * @see ImagingParameters#parameters * * @since 2.4 */ protected ParameterBlockJAI prepareParameters(final ParameterValueGroup parameters) { final ImagingParameters copy = (ImagingParameters) descriptor.createValue(); final ParameterBlockJAI block = (ParameterBlockJAI) copy.parameters; org.apache.sis.parameter.Parameters.copy(parameters, copy); return block; } /** * Applies a process operation to a grid coverage. * The default implementation performs the following steps: * <p> * <ol> * <li>Converts source Grid Coverages to their {@linkplain #getComputationView * computation view} (typically {@linkplain ViewType#GEOPHYSICS geophysics}).</li> * * <li>Converts every sources Coverages to the same {@linkplain CoordinateReferenceSystem * Coordinate Reference System} (at least for the two-dimensional part) with the same * "{@linkplain GridGeometry2D#getGridToCRS2D grid to CRS}" relationship.</li> * * <li>Invokes {@link #deriveGridCoverage deriveGridCoverage}. * The sources in the {@code ParameterBlock} are {@link RenderedImage} objects * obtained from {@link GridCoverage2D#getRenderedImage()}.</li> * * <li>If a change of view has been performed at step 1, converts the result back to the * original view.</li> * </ol> * * @param parameters List of name value pairs for the parameters required for the operation. * @param hints A set of rendering hints, or {@code null} if none. * @return The result as a grid coverage. * @throws CoverageProcessingException If an error occurred while applying the operation. * * @see #deriveGridCoverage */ @Override protected Coverage doOperation(final ParameterValueGroup parameters, final Hints hints) throws CoverageProcessingException { final ParameterBlockJAI block = prepareParameters(parameters); /* * Extracts the source grid coverages now as an array. The sources will be set in the * ParameterBlockJAI (as RenderedImages) later, after the reprojection performed in the * next block. */ final String[] sourceNames = operation.getSourceNames(); final GridCoverage2D[] sources = new GridCoverage2D[sourceNames.length]; ViewType primarySourceType = extractSources(parameters, sourceNames, sources); /* * Ensures that all coverages use the same CRS and has the same 'gridToCRS' relationship. * After the reprojection, the method still checks all CRS in case the user overridden the * {@link #resampleToCommonGeometry} method. */ resampleToCommonGeometry(sources, null, null, hints); GridCoverage2D coverage = sources[PRIMARY_SOURCE_INDEX]; final CoordinateReferenceSystem crs = coverage.getCoordinateReferenceSystem2D(); final MathTransform2D gridToCRS = coverage.getGridGeometry().getGridToCRS2D(); for (int i=0; i<sources.length; i++) { final GridCoverage2D source = sources[i]; if (!Utilities.equalsIgnoreMetadata(crs, source.getCoordinateReferenceSystem2D()) || !Utilities.equalsIgnoreMetadata(gridToCRS, source.getGridGeometry().getGridToCRS2D())) { throw new IllegalArgumentException(Errors.format(Errors.Keys.IncompatibleGridGeometry)); } block.setSource(sourceNames[i], source.getRenderedImage()); } /* * Applies the operation. This delegates the work to the chain of 'deriveXXX' methods. */ coverage = deriveGridCoverage(sources, new Parameters(crs, gridToCRS, block, hints)); if (primarySourceType != null) { coverage = coverage.view(primarySourceType); } return coverage; } /** * Returns an error message for an unsupported CRS. */ private static String unsupported(final CoordinateReferenceSystem crs) { return Errors.format(Errors.Keys.UnsupportedCrs_1, crs.getName().getCode()); } /** * Returns a sub-coordinate reference system for the specified dimension range. * This method is for internal use by {@link #resampleToCommonGeometry}. * * @param crs The coordinate reference system to decompose. * @param lower The first dimension to keep, inclusive. * @param upper The last dimension to keep, exclusive. * @return The sub-coordinate system, or {@code null} if {@code lower} is equals to {@code upper}. * @throws InvalidGridGeometryException if the CRS can't be separated. */ private static CoordinateReferenceSystem getSubCRS(final CoordinateReferenceSystem crs, final int lower, final int upper) throws InvalidGridGeometryException { if (lower == upper) { return null; } final CoordinateReferenceSystem candidate = org.apache.sis.referencing.CRS.getComponentAt(crs, lower, upper); if (candidate == null) { throw new InvalidGridGeometryException(unsupported(crs)); } return candidate; } /** * Ensures that the source and target dimensions are the same. This method is for internal * use by {@link #resampleToCommonGeometry}. */ private static void ensureStableDimensions(final DimensionFilter filter) throws InvalidGridGeometryException { final int[] source = filter.getSourceDimensions(); Arrays.sort(source); final int[] target = filter.getTargetDimensions(); Arrays.sort(target); if (!Arrays.equals(source, target)) { throw new InvalidGridGeometryException(Errors.format(Errors.Keys.UnsupportedTransform)); } } /** * Resamples all sources grid coverages to the same {@linkplain GridGeometry2D two-dimensional * geometry} before to apply the {@linkplain #operation}. This method is invoked automatically * by the {@link #doOperation doOperation} method. * <p> * Only the two-dimensional part is reprojected (usually the spatial component of a CRS). * Extra dimensions (if any) are left unchanged. Extra dimensions are typically time axis * or depth. Note that extra dimensions are <strong>not</strong> forced to a common geometry; * only the two dimensions that apply to a {@link javax.media.jai.PlanarImage} are. This is * because the extra dimensions don't need to be compatible for all operations. For example * if a source image is a slice in a time series, a second source image could be a slice in * the frequency representation of this time series. * <p> * Subclasses should override this method if they want to specify target * {@linkplain GridGeometry2D Grid Geometry} and * {@linkplain CoordinateReferenceSystem Coordinate Reference System} different than the * default ones. For example if a subclass wants to force all images to be referenced in a * {@linkplain org.geotoolkit.referencing.crs.DefaultGeographicCRS#WGS84 WGS 84} CRS, then * it may overrides this method as below: * * {@preformat java * protected void resampleToCommonGeometry(...) { * crs2D = CommonCRS.WGS84.normalizedGeographic(); * super.resampleToCommonGeometry(sources, crs2D, gridToCrs2D, hints); * } * } * * @param sources The source grid coverages to resample. This array is updated in-place as * needed (for example if a grid coverage is replaced by a projected one). * @param crs2D The target coordinate reference system to use, or {@code null} for a * default one. * @param gridToCrs2D The target "<cite>grid to CRS"</cite> transform, or {@code null} * for a default one. * @param hints The rendering hints, or {@code null} if none. * * @throws InvalidGridGeometryException If a source coverage has an unsupported grid geometry. * @throws CannotReprojectException If a grid coverage can't be resampled for some other reason. */ protected void resampleToCommonGeometry(final GridCoverage2D[] sources, CoordinateReferenceSystem crs2D, MathTransform2D gridToCrs2D, final Hints hints) throws InvalidGridGeometryException, CannotReprojectException { if (sources == null || sources.length == 0) { return; // Nothing to reproject. } /* * Ensures that the target CRS is two-dimensional. If no target CRS were specified, * uses the CRS of the primary source. The math transform must be 2D too, but this * is ensured by the interface type (MathTransform2D). */ final GridCoverage2D primarySource = sources[PRIMARY_SOURCE_INDEX]; if (crs2D == null) { if (gridToCrs2D == null && sources.length == 1) { return; // No need to reproject. } crs2D = primarySource.getCoordinateReferenceSystem2D(); } else try { crs2D = CRSUtilities.getCRS2D(crs2D); } catch (TransformException exception) { throw new CannotReprojectException(unsupported(crs2D), exception); } if (gridToCrs2D == null) { gridToCrs2D = primarySource.getGridGeometry().getGridToCRS2D(); } /* * 'crs2D' is the two dimensional part of the target CRS. Now for each source coverages, * substitute their two-dimensional CRS by this 'crs2D'. A source may have more than two * dimensions. For example it may have a time or a depth axis. In such case, their "head" * and "tail" CRS will be preserved before and after 'crs2D'. */ final AbstractCoverageProcessor processor = getProcessor(hints); for (int i=0; i<sources.length; i++) { final GridCoverage2D source = sources[i]; final GridGeometry2D geometry = source.getGridGeometry(); final CoordinateReferenceSystem srcCrs2D = source.getCoordinateReferenceSystem2D(); final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem(); final CoordinateReferenceSystem targetCRS; if (Utilities.equalsIgnoreMetadata(crs2D, srcCrs2D)) { targetCRS = sourceCRS; // No reprojection needed for this source coverage. } else { /* * Replaces the 2D part in the source CRS, while preserving the leading and * trailing CRS (if any). Leading and trailing CRS are typically time axis or * depth axis. Current implementation requires that the 2D part appears in two * consecutive dimensions. Those dimensions are (0,1) in the majority of cases. */ final int lowerDim = Math.min(geometry.axisDimensionX, geometry.axisDimensionY); final int upperDim = Math.max(geometry.axisDimensionX, geometry.axisDimensionY)+1; final int sourceDim = sourceCRS.getCoordinateSystem().getDimension(); if (upperDim-lowerDim != srcCrs2D.getCoordinateSystem().getDimension()) { throw new InvalidGridGeometryException(unsupported(sourceCRS)); } final CoordinateReferenceSystem headCRS = getSubCRS(sourceCRS, 0, lowerDim); final CoordinateReferenceSystem tailCRS = getSubCRS(sourceCRS, upperDim, sourceDim); CoordinateReferenceSystem[] components = new CoordinateReferenceSystem[3]; int count = 0; if (headCRS != null) components[count++] = headCRS; components[count++] = crs2D; if (tailCRS != null) components[count++] = tailCRS; components = ArraysExt.resize(components, count); if (count == 1) { targetCRS = components[0]; } else try { targetCRS = FactoryFinder.getCRSFactory(hints).createCompoundCRS( Collections.singletonMap(IdentifiedObject.NAME_KEY, crs2D.getName().getCode()), components); } catch (FactoryException exception) { throw new CannotReprojectException(exception); } } /* * Constructs the 'gridToCRS' transform in the same way than the CRS: * leading and trailing dimensions (if any) are preserved. */ final MathTransform toSource2D = geometry.getGridToCRS2D(); final MathTransform toSource = geometry.getGridToCRS(); MathTransform toTarget; if (Utilities.equalsIgnoreMetadata(gridToCrs2D, toSource2D)) { toTarget = toSource; } else { /* * Replaces the 2D part in the source MT, while preserving the leading and * trailing MT (if any). This is similar to the 'lowerDim' and 'upperDim' * variables in the CRS case above, except that we operate on "grid" space * rather than "axis" spaces. The index are usually the same, but not always. */ final int lowerDim = Math.min(geometry.gridDimensionX, geometry.gridDimensionY); final int upperDim = Math.max(geometry.gridDimensionX, geometry.gridDimensionY)+1; final int sourceDim = toSource.getSourceDimensions(); if (upperDim-lowerDim != toSource2D.getSourceDimensions()) { throw new InvalidGridGeometryException(Errors.format(Errors.Keys.UnsupportedTransform)); } final MathTransformFactory factory = FactoryFinder.getMathTransformFactory(hints); final DimensionFilter filter = new DimensionFilter(toSource, factory); toTarget = gridToCrs2D; try { if (lowerDim != 0) { filter.addSourceDimensionRange(0, lowerDim); MathTransform step = filter.separate(); ensureStableDimensions(filter); step = factory.createPassThroughTransform(0, step, sourceDim-lowerDim); toTarget = factory.createConcatenatedTransform(step, toTarget); } if (upperDim != sourceDim) { filter.clear(); filter.addSourceDimensionRange(upperDim, sourceDim); MathTransform step = filter.separate(); ensureStableDimensions(filter); step = factory.createPassThroughTransform(upperDim, step, 0); toTarget = factory.createConcatenatedTransform(toTarget, step); } } catch (FactoryException exception) { throw new CannotReprojectException(Errors.format( Errors.Keys.CantReprojectCoverage_1, source.getName()), exception); } } final GridGeometry2D targetGeom = new GridGeometry2D(null, toTarget, targetCRS); final ParameterValueGroup param = processor.getOperation("Resample").getParameters(); param.parameter("Source") .setValue(source); param.parameter("GridGeometry") .setValue(targetGeom); param.parameter("CoordinateReferenceSystem").setValue(targetCRS); sources[i] = (GridCoverage2D) processor.doOperation(param); } } /** * Applies a JAI operation to a grid coverage. This method is invoked automatically by * the {@link #doOperation doOperation} method. The default implementation performs the * following steps: * <p> * <ul> * <li>Gets the {@linkplain GridSampleDimension Sample Dimensions} for the target images * by invoking the {@link #deriveSampleDimension deriveSampleDimension} method.</li> * <li>Applies the JAI operation using {@link #createRenderedImage 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 #deriveSampleDimension * @see JAI#createNS */ protected GridCoverage2D deriveGridCoverage( final GridCoverage2D[] sources, final Parameters parameters) { GridCoverage2D primarySource = sources[PRIMARY_SOURCE_INDEX]; /* * Gets the target SampleDimensions. If they are identical to the SampleDimensions of * one of the source GridCoverage2D, then this GridCoverage2D will be used at the primary * source. It will affect the target GridCoverage2D's name and the visible band. Then, * a new color model will be constructed from the new SampleDimensions, taking in * account the visible band. */ final GridSampleDimension[][] list = new GridSampleDimension[sources.length][]; for (int i=0; i<list.length; i++) { list[i] = sources[i].getSampleDimensions(); } final GridSampleDimension[] sampleDims = deriveSampleDimension(list, parameters); int primarySourceIndex = -1; for (int i=0; i<list.length; i++) { if (Arrays.equals(sampleDims, list[i])) { primarySource = sources[i]; primarySourceIndex = i; break; } } /* * Sets the rendering hints image layout. Only the following properties will be set: * * - Color model */ RenderingHints hints = ImageUtilities.getRenderingHints(parameters.getSource()); ImageLayout layout = (hints!=null) ? (ImageLayout)hints.get(JAI.KEY_IMAGE_LAYOUT) : null; if (layout==null || !layout.isValid(ImageLayout.COLOR_MODEL_MASK)) { if (sampleDims!=null && sampleDims.length!=0) { int visibleBand = CoverageUtilities.getVisibleBand(primarySource.getRenderedImage()); if (visibleBand >= sampleDims.length) { visibleBand = 0; } final ColorModel colors; colors = sampleDims[visibleBand].getColorModel(visibleBand, sampleDims.length); if (colors != null) { if (layout == null) { layout = new ImageLayout(); } layout = layout.setColorModel(colors); } } } if (layout != null) { if (hints == null) { hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout); } else { hints.put(JAI.KEY_IMAGE_LAYOUT, layout); } } if (parameters.hints != null) { if (hints != null) { hints.add(parameters.hints); // May overwrite the image layout we have just set. } else { hints = parameters.hints; } } /* * 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, primarySourceIndex, parameters); final CoordinateReferenceSystem crs = primarySource.getCoordinateReferenceSystem(); final MathTransform toCRS = primarySource.getGridGeometry().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). sampleDims, // The sample dimensions sources, // The source grid coverages. properties); // Properties } /** * Prepares the properties to be given to the coverage created by the * {@link #deriveGridCoverage deriveGridCoverage} method. The default * implementation returns {@code null}. * * @param data The {@link RenderedImage} created by this operation. * @param crs The coordinate reference system assigned to the coverage this * {@code OperationJAI} will produce. * @param name The name assigned to the coverage this {@code OperationJAI} will produce. * @param gridToCRS The {@linkplain MathTransform transform} from grid to {@code crs} to be * assigned to the coverage this {@link OperationJAI} will produce. * @param sources The sources to be assigned to the coverage this {@link OperationJAI} will * produce. * @param parameters The parameters that were used by this {@link OperationJAI}. * @return A {@code Map} with the properties generated by this {@link OperationJAI}, * or null if we haven't any. * * @since 2.4 */ protected Map<String,?> getProperties(RenderedImage data, CoordinateReferenceSystem crs, InternationalString name, MathTransform gridToCRS, GridCoverage2D[] sources, Parameters parameters) { return null; } /** * Returns the index of the quantitative category, providing that there is * one and only one quantitative category. If {@code categories} contains 0, * 2 or more quantitative category, then this method returns {@code -1}. * * @param categories The categories to test. * @return The index of the quantitative category, or {@code -1} if none can be chosen. * * @since 2.4 */ protected static int getQuantitative(final Category[] categories) { int index = -1; for (int i=0; i<categories.length; i++) { if (categories[i].isQuantitative()) { if (index >= 0) { return -1; } index = i; } } return index; } /** * Returns the {@linkplain GridSampleDimension sample dimensions} for the target * {@linkplain GridCoverage2D grid coverage}. This method is invoked automatically by * {@link #deriveGridCoverage deriveGridCoverage} with a {@code bandLists} argument * initialized as below: * <p> * <ul> * <li>The {@code bandLists} array length is equals to the number of source coverages.</li> * <li>The <code>bandLists[<var>i</var>]</code> array length is equals to the number of * sample dimensions in the source coverage <var>i</var>.</li> * <li>The sample dimension for a band at index <var>band</var> in the source at index * <var>source</var> is {@code bandLists[source][band]}.</li> * </ul> * <p> * This method shall return an array with a length equals to the number of bands in the target * image. If the sample dimensions can't be determined, then this method is allowed to returns * {@code null}. * <p> * The default implementation iterates among all bands and invokes the {@link #deriveCategory * deriveCategory} and {@link #deriveUnit deriveUnit} methods for each of them. Subclasses * should override this method if they know a more accurate algorithm for determining sample * dimensions. * * @param bandLists The set of sample dimensions for each source {@link GridCoverage2D}s. * @param parameters Parameters, rendering hints and coordinate reference system to use. * @return The sample dimensions for each band in the destination image, or {@code null} * if unknown. * * @see #deriveCategory * @see #deriveUnit */ protected GridSampleDimension[] deriveSampleDimension( final GridSampleDimension[][] bandLists, final Parameters parameters) { /* * Computes the number of bands. Sources with only 1 band are treated as a special case: * their unique band is applied to all bands in other sources. If sources don't have the * same number of bands, then this method returns {@code null} since we don't know how to * handle those cases. */ int numBands = 1; for (int i=0; i<bandLists.length; i++) { final int nb = bandLists[i].length; if (nb != 1) { if (numBands!=1 && nb!=numBands) { return null; } numBands = nb; } } /* * Iterates among all bands. The 'result' array will contains SampleDimensions created * during the iteration for each individual band. The 'XS' suffix designates temporary * arrays of categories and units accross all sources for one particular band. */ final GridSampleDimension[] result = new GridSampleDimension[numBands]; final Category[] categoryXS = new Category[bandLists.length]; final Unit<?>[] unitXS = new Unit<?>[bandLists.length]; while (--numBands >= 0) { GridSampleDimension sampleDim = null; Category[] categoryArray = null; int indexOfQuantitative = 0; assert PRIMARY_SOURCE_INDEX == 0; // See comment below. for (int i=bandLists.length; --i>=0;) { /* * Iterates among all sources (i) for the current band. We iterate * sources in reverse order because the primary source MUST be the * last one iterated, in order to have proper values for variables * 'sampleDim', 'categoryArray' and 'indexOfQuantitative' after the * loop. */ final GridSampleDimension[] allBands = bandLists[i]; sampleDim = allBands[allBands.length == 1 ? 0 : numBands]; final List<Category> categories = sampleDim.getCategories(); // GridSampleDimension may contain no categories if (categories == null) { result[numBands] = sampleDim; continue; } categoryArray = (Category[]) categories.toArray(); // NOSONAR indexOfQuantitative = getQuantitative(categoryArray); if (indexOfQuantitative < 0) { return null; } unitXS [i] = sampleDim.getUnits(); categoryXS[i] = categoryArray[indexOfQuantitative]; } if (categoryArray == null) { continue; } final Category oldCategory = categoryArray[indexOfQuantitative]; final Unit<?> oldUnit = sampleDim.getUnits(); final Category newCategory = deriveCategory(categoryXS, parameters); final Unit<?> newUnit = deriveUnit(unitXS, parameters); if (newCategory == null) { return null; } if (!oldCategory.equals(newCategory) || !Objects.equals(oldUnit, newUnit)) { /* * Create a new sample dimension. Note that we use a null title, not the same * title than the original sample dimension, because the new sample dimension * may be quite different. For example the original sample dimension may be * about "Temperature" in °C units, and the new one about "Gradient magnitude * of Temperature" in °C/km units. The GridSampleDimension constructor will * infers the title from what looks like the "main" category. */ final CharSequence title = null; categoryArray[indexOfQuantitative] = newCategory; result[numBands] = new GridSampleDimension(title, categoryArray, newUnit); } else { // Reuse the category list from the primary source. result[numBands] = sampleDim; } } return result; } /** * Returns the quantitative category for a single {@linkplain GridSampleDimension sample dimension} * in the target {@linkplain GridCoverage2D grid coverage}. This method is invoked automatically * by the {@link #deriveSampleDimension deriveSampleDimension} method for each band in the * target image. The default implementation creates a default category from the target range * of values returned by {@link #deriveRange deriveRange}. * * @param categories The quantitative categories from every sources. For unary operations * like {@code "GradientMagnitude"}, this array has a length of 1. For binary * operations like {@code "add"} and {@code "multiply"}, this array has a length of 2. * @param parameters Parameters, rendering hints and coordinate reference system to use. * @return The quantitative category to use in the destination image, or {@code null} if unknown. */ protected Category deriveCategory(final Category[] categories, final Parameters parameters) { final NumberRange<?>[] ranges = new NumberRange<?>[categories.length]; for (int i=0; i<ranges.length; i++) { ranges[i] = categories[i].getRange(); } final NumberRange<?> range = deriveRange(ranges, parameters); if (range != null) { final Category category = categories[PRIMARY_SOURCE_INDEX]; return new Category(category.getName(), category.getColors(), category.geophysics(false).getRange(), range).geophysics(true); } return null; } /** * Returns the range of value for a single {@linkplain GridSampleDimension sample dimension} * in the target {@linkplain GridCoverage2D grid coverage}. This method is invoked automatically * by the {@link #deriveCategory deriveCategory} method for each band in the target image. * Subclasses should override this method in order to compute the target range of values. * For example, the {@code "add"} operation may implements this method as below: * * {@preformat java * double min = ranges[0].getMinimum() + ranges[1].getMinimum(); * double max = ranges[0].getMaximum() + ranges[1].getMaximum(); * return NumberRange.create(min, max); * } * * @param ranges The range of values from every sources. For unary operations like * {@code "GradientMagnitude"}, this array has a length of 1. For binary operations * like {@code "add"} and {@code "multiply"}, this array has a length of 2. * @param parameters Parameters, rendering hints and coordinate reference system to use. * @return The range of values to use in the destination image, or {@code null} if unknown. */ protected NumberRange<?> deriveRange(final NumberRange<?>[] ranges, final Parameters parameters) { return null; } /** * Returns the unit of data for a single {@linkplain GridSampleDimension sample dimension} in the * target {@linkplain GridCoverage2D grid coverage}. This method is invoked automatically by * the {@link #deriveSampleDimension deriveSampleDimension} method for each band in the target * image. Subclasses should override this method in order to compute the target units from the * source units. For example a {@code "multiply"} operation may implement this method as below: * * {@preformat java * if (units[0] != null && units[1] != null) { * return units[0].times(units[1]); * } else { * return super.deriveUnit(units, cs, parameters); * } * } * * @param units The units from every sources. For unary operations like * {@code "GradientMagnitude"}, this array has a length of 1. For binary operations * like {@code "add"} and {@code "multiply"}, this array has a length of 2. * @param parameters Parameters, rendering hints and coordinate reference system to use. * @return The unit of data in the destination image, or {@code null} if unknown. */ protected Unit<?> deriveUnit(final Unit<?>[] units, final Parameters parameters) { return null; } /** * Returns a name for the target {@linkplain GridCoverage2D grid coverage} based on the given * sources. This method is invoked once by the {@link #deriveGridCoverage deriveGridCoverage} * method. The default implementation returns the operation name followed by the source name * between parenthesis, for example "<cite>GradientMagnitude(Sea Surface Temperature)</cite>". * * @param sources The sources grid coverage. * @param primarySourceIndex The index of what seems to be the primary source, or {@code -1} * if none of unknown. * @param parameters Parameters, rendering hints and coordinate reference system to use. * @return A name for the target grid coverage. */ protected InternationalString deriveName(final GridCoverage2D[] sources, final int primarySourceIndex, final Parameters parameters) { final InternationalString[] names; if (primarySourceIndex >= 0) { names = new InternationalString[] { sources[primarySourceIndex].getName() }; } else { names = new InternationalString[sources.length]; for (int i=0; i<names.length; i++) { names[i] = sources[i].getName(); } } return new Name(getName(), names); } /** * A localized name for the default implementation of {@link OperationJAI#deriveName}. */ private static final class Name extends AbstractInternationalString implements Serializable { /** Serial number for cross-versions compatibility. */ private static final long serialVersionUID = -8096255331549347383L; /** The operation name. */ private final String operation; /** Names of source grid coverages. */ private final InternationalString[] sources; /** Constructs a name from the given source names. */ public Name(final String operation, final InternationalString[] sources) { this.operation = operation; this.sources = sources; } /** Returns a string localized in the given locale. */ @Override public String toString(final Locale locale) { final StringBuilder buffer = new StringBuilder(operation); buffer.append('('); for (int i=0; i<sources.length; i++) { if (i != 0) { buffer.append(", "); } buffer.append(sources[i].toString(locale)); } return buffer.append(')').toString(); } } /** * Applies the JAI operation. The operation name can be fetch from {@link #operation}. * The JAI instance to use can be fetch from {@link #getJAI}. The default implementation * returns the following: * * {@preformat java * getJAI(hints).createNS(operation.getName(), parameters, hints) * } * * Subclasses may override this method in order to invokes a different JAI operation * according the parameters. * * @param parameters The parameters to be given to JAI. * @param hints The rendering hints to be given to JAI. * @return The result of JAI operation using the given parameters and hints. */ protected RenderedImage createRenderedImage(final ParameterBlockJAI parameters, final RenderingHints hints) { return getJAI(hints).createNS(operation.getName(), parameters, hints); } /** * Returns the {@link JAI} instance to use for operations on {@link RenderedImage}. * If no JAI instance is defined for the {@link Hints#JAI_INSTANCE} key, then the * default instance is returned. * * @param hints The rendering hints, or {@code null} if none. * @return The JAI instance to use (never {@code null}). */ public static JAI getJAI(final RenderingHints hints) { if (hints != null) { final Object value = hints.get(Hints.JAI_INSTANCE); if (value instanceof JAI) { return (JAI) value; } } return JAI.getDefaultInstance(); } /** * Compares the specified object with this operation for equality. */ @Override public boolean equals(final Object object) { if (object == this) { // Slight optimization return true; } if (super.equals(object)) { final OperationJAI that = (OperationJAI) object; return Objects.equals(this.operation, that.operation); } return false; } /** * A block of parameters for a {@link GridCoverage2D} processed by a {@link OperationJAI}. * This parameter is given to the following methods: * <p> * <ul> * <li>{@link OperationJAI#deriveSampleDimension deriveSampleDimension}</li> * <li>{@link OperationJAI#deriveCategory deriveCategory}</li> * <li>{@link OperationJAI#deriveUnit deriveUnit}</li> * </ul> * * @author Martin Desruisseaux (IRD) * @version 3.00 * * @since 2.2 * @module */ protected static final class Parameters { /** * The two dimensional coordinate reference system for all sources and the * destination {@link GridCoverage2D}. Sources coverages will be projected in * this CRS as needed. */ public final CoordinateReferenceSystem crs; /** * The "<cite>grid to CRS"</cite> transform common to all source grid coverages. */ public final MathTransform2D gridToCRS; /** * The parameters to be given to the {@link JAI#createNS} method. */ public final ParameterBlockJAI parameters; /** * 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 parameter block with the specified values. */ Parameters(final CoordinateReferenceSystem crs, final MathTransform2D gridToCRS, final ParameterBlockJAI parameters, final Hints hints) { this.crs = crs; this.gridToCRS = gridToCRS; this.parameters = parameters; this.hints = hints; } /** * 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; } } }