/* * 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.Map; import java.util.Locale; import java.util.Arrays; import java.util.TreeMap; import java.util.Iterator; import java.util.Collection; import java.util.Comparator; import java.util.logging.Level; import java.util.logging.LogRecord; import java.awt.RenderingHints; import javax.media.jai.JAI; import javax.media.jai.TileCache; import javax.media.jai.Interpolation; import org.opengis.coverage.Coverage; import org.opengis.coverage.processing.Operation; import org.opengis.coverage.processing.OperationNotFoundException; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.GeneralParameterValue; import org.geotoolkit.factory.Hints; import org.geotoolkit.factory.FactoryRegistry; import org.geotoolkit.coverage.grid.GridCoverage2D; import org.geotoolkit.coverage.grid.Interpolator2D; import org.geotoolkit.internal.FactoryUtilities; import org.geotoolkit.resources.Loggings; import org.geotoolkit.resources.Errors; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** * Default implementation of a {@linkplain Coverage} processor. * This default implementation makes the following assumptions: * <p> * <ul> * <li>Operations are declared in the * {@code META-INF/services/org.opengis.coverage.processing.Operation} file.</li> * <li>Operations are actually instances of {@link AbstractOperation} (note: this * constraint may be relaxed in a future version after GeoAPI interfaces for grid * coverage will be redesigned).</li> * <li>Operation parameter names are case-insensitive.</li> * <li>Most operations are backed by <cite>Java Advanced Imaging</cite>.</li> * </ul> * <p> * <strong>Note:</strong> This implementation does not cache produced coverages. * Since coverages may be big, consider wrapping {@code DefaultCoverageProcessor} * instances in {@link CachingCoverageProcessor}. * * @author Martin Desruisseaux (IRD) * @version 3.02 * * @since 1.2 * @module */ public class DefaultCoverageProcessor extends AbstractCoverageProcessor { /** * Augments the amout of memory allocated for the JAI tile cache. */ static { final long targetCapacity = 0x4000000; // 64 Mo. final long maxMemory = Runtime.getRuntime().maxMemory(); final TileCache cache = JAI.getDefaultInstance().getTileCache(); if (maxMemory >= 2*targetCapacity) { if (cache.getMemoryCapacity() < targetCapacity) { cache.setMemoryCapacity(targetCapacity); } } final float mb = cache.getMemoryCapacity() / (1024f * 1024f); LOGGER.log(Level.CONFIG, "Java Advanced Imaging: {0}, TileCache capacity={1} Mb", new Object[] {JAI.getBuildVersion(), mb}); /* * Verifies that the tile cache has some reasonable value. A lot of users seem to * misunderstand the memory setting in Java and set wrong values. If the user set * a tile cache greater than the maximum heap size, tell him that he is looking * for serious trouble. */ if (cache.getMemoryCapacity() + (4*1024*1024) >= maxMemory) { final LogRecord record = Loggings.format(Level.SEVERE, Loggings.Keys.ExcessiveTileCache_1, maxMemory / (1024 * 1024.0)); record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } } /** * The comparator for ordering operation names. */ private static final Comparator<String> COMPARATOR = new Comparator<String>() { @Override public int compare(final String name1, final String name2) { return name1.toLowerCase(Locale.US).compareTo(name2.toLowerCase(Locale.US)); } }; /** * The set of operations for this coverage processor. Keys are operation's name. * Values are operations and should not contains duplicated values. Note that while * keys are {@link String} objects, the operation name are actually case-insensitive * because of the comparator used in the sorted map. */ private final Map<String,Operation> operations = new TreeMap<>(COMPARATOR); /** * The service registry for finding {@link Operation} implementations. */ private final FactoryRegistry registry; /** * The processor to declare in the {@link Hints#GRID_COVERAGE_PROCESSOR}. */ private final AbstractCoverageProcessor declaredProcessor; /** * Constructs a coverage processor from the given hints. The {@link #scanForPlugins} * method will be automatically invoked the first time an operation is required. * Additional operations can be added by subclasses with the {@link #addOperation} method. * <p> * Rendering hints will be initialized with the following hints: * <p> * <ul> * <li>{@link JAI#KEY_REPLACE_INDEX_COLOR_MODEL} set to {@link Boolean#FALSE}.</li> * <li>{@link JAI#KEY_TRANSFORM_ON_COLORMAP} set to {@link Boolean#FALSE}.</li> * </ul> * * @param userHints A set of additional rendering hints, or {@code null} if none. */ public DefaultCoverageProcessor(final Hints userHints) { this(userHints, null); } /** * For {@link CachingCoverageProcessor} usage. */ DefaultCoverageProcessor(final Hints userHints, AbstractCoverageProcessor declaredProcessor) { registry = new FactoryRegistry(Arrays.asList(new Class<?>[] { Operation.class })); final Map<RenderingHints.Key, Object> hints = this.hints; hints.put(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE); hints.put(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.FALSE); /* * The following hints are relevant to some operations. We declare them explicitly, * with null value (meaning "undefined"), in order to inform FactoryRegistry that if * those hints are supplied by the user, then they should be taken in account. Next * we override this default setting by the user-supplied one, if any. */ hints.put(Hints.COORDINATE_OPERATION_FACTORY, null); hints.put(Hints.LENIENT_DATUM_SHIFT, null); hints.put(Hints.DATUM_SHIFT_METHOD, null); FactoryUtilities.addImplementationHints(userHints, hints); hints.remove(Hints.GRID_COVERAGE_PROCESSOR); // Must erase user setting. if (declaredProcessor == null) { declaredProcessor = this; } this.declaredProcessor = declaredProcessor; } /** * Adds the specified operation to this processor. This method is usually invoked * at construction time before this processor is made accessible. * * @param operation The operation to add. * @throws IllegalStateException if an operation already exists with the same name. */ protected synchronized void addOperation(final Operation operation) throws IllegalStateException { ensureNonNull("operation", operation); if (operations.isEmpty()) { scanForPlugins(); } addOperationImpl(operation); } /** * Implementation of {@link #addOperation} method. Also used by {@link #scanForPlugins} * instead of the public method in order to avoid never-ending loop. */ private void addOperationImpl(final Operation operation) throws IllegalStateException { final String name = operation.getName().trim(); final Operation old = operations.put(name, operation); if (old!=null && !old.equals(operation)) { operations.put(old.getName().trim(), old); throw new IllegalStateException(Errors.getResources(getLocale()).getString( Errors.Keys.OperationAlreadyBounds_1, operation.getName())); } } /** * Retrieves grid processing operations information. Each operation information contains * the name of the operation as well as a list of its parameters. */ @Override public synchronized Collection<Operation> getOperations() { if (operations.isEmpty()) { scanForPlugins(); } return operations.values(); } /** * Returns the operation for the specified name. * * @param name Name of the operation (case insensitive). * @return The operation for the given name. * @throws OperationNotFoundException if there is no operation for the specified name. */ @Override public synchronized Operation getOperation(String name) throws OperationNotFoundException { ensureNonNull("name", name); name = name.trim(); if (operations.isEmpty()) { scanForPlugins(); } final Operation operation = operations.get(name); if (operation != null) { return operation; } throw new OperationNotFoundException(Errors.getResources(getLocale()).getString( Errors.Keys.NoSuchOperation_1, name)); } /** * Returns a rendering hint. * * @param key The hint key (e.g. {@link Hints#JAI_INSTANCE}). * @return The hint value for the specified key, or {@code null} if there is no hint for the * specified key. */ public final Object getRenderingHint(final RenderingHints.Key key) { return hints.get(key); } /** * Applies a process operation to a coverage. The default implementation checks if source * coverages use an interpolation, and then invokes {@link AbstractOperation#doOperation}. * If all source coverages used the same interpolation, then this interpolation is applied * to the resulting coverage (except if the resulting coverage has already an interpolation). * * @param parameters Parameters required for the operation. The easiest way to construct them * is to invoke <code>operation.{@link Operation#getParameters getParameters}()</code> * and to modify the returned group. * @return The result as a coverage. * @throws OperationNotFoundException if there is no operation for the parameter group name. * @throws CoverageProcessingException if the operation can not be executed. */ @Override public synchronized Coverage doOperation(final ParameterValueGroup parameters) throws CoverageProcessingException, OperationNotFoundException { Coverage source = getPrimarySource(parameters); final String operationName = getOperationName(parameters); final Operation operation = getOperation(operationName); /* * Detects the interpolation type for the source grid coverage. * The same interpolation will be applied on the result. */ Interpolation[] interpolations = null; if (!operationName.equalsIgnoreCase("Interpolate")) { for (final GeneralParameterValue param : parameters.values()) { if (param instanceof ParameterValue<?>) { final Object value = ((ParameterValue<?>) param).getValue(); if (value instanceof Interpolator2D) { // If all sources use the same interpolation, preserves the // interpolation for the resulting coverage. Otherwise, uses // the default interpolation (nearest neighbor). final Interpolation[] interp = ((Interpolator2D) value).getInterpolations(); if (interpolations == null) { interpolations = interp; } else if (!Arrays.equals(interpolations, interp)) { // Set to no interpolation. interpolations = null; break; } } } } } /* * Gets the hints to be given to the operation. Note that the DefaultCoverageProcessor * constructor has explicitly set to null a few hints considered relevant, while not * necessarily specified by the user. If those hints were not overridden by the user, * then the corresponding null value will not be put in the hints map below. */ final Hints hints = EMPTY_HINTS.clone(); FactoryUtilities.addValidEntries(this.hints, hints, true); hints.put(Hints.GRID_COVERAGE_PROCESSOR, declaredProcessor); /* * Applies the operation, applies the same interpolation and log a message. * Note: we don't use "if (operation instanceof AbstractOperation)" below * because if it is not, we want the ClassCastException as the cause for the failure. */ final AbstractOperation op; try { op = (AbstractOperation) operation; } catch (ClassCastException cause) { throw new OperationNotFoundException(Errors.getResources(getLocale()).getString( Errors.Keys.NoSuchOperation_1, operationName), cause); } Coverage cv = op.doOperation(parameters, hints); if (interpolations != null && (cv instanceof GridCoverage2D) && !(cv instanceof Interpolator2D)) { cv = Interpolator2D.create((GridCoverage2D) cv, interpolations); } log(source, cv, operationName, false); return cv; } /** * Scans for factory plug-ins on the application class path. This method is needed because the * application class path can theoretically change, or additional plug-ins may become available. * Rather than re-scanning the classpath on every invocation of the API, the class path is * scanned automatically only on the first invocation. Clients can call this method to prompt * a re-scan. Thus this method need only be invoked by sophisticated applications which * dynamically make new plug-ins available at runtime. */ public synchronized void scanForPlugins() { final Iterator<Operation> it = registry.getServiceProviders(Operation.class, null, null, null); while (it.hasNext()) { final Operation operation = it.next(); final String name = operation.getName().trim(); if (!operations.containsKey(name)) { addOperationImpl(operation); } } } }