/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.coverage.processing;
import java.awt.RenderingHints;
import java.awt.image.RenderedImage;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.TileCache;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.Interpolator2D;
import org.geotools.factory.FactoryRegistry;
import org.geotools.factory.Hints;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.LoggingKeys;
import org.geotools.resources.i18n.Loggings;
import org.opengis.coverage.Coverage;
import org.opengis.coverage.processing.Operation;
import org.opengis.coverage.processing.OperationNotFoundException;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
/**
* Default implementation of a {@linkplain Coverage 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>Most operations are backed by <cite>Java Advanced Imaging</cite>.</li>
* </ul>
* <p>
* <strong>Note:</strong> This implementation do not caches produced coverages. Since coverages
* may be big, consider wrapping {@code DefaultProcessor} instances in {@link BufferedProcessor}.
*
* @since 2.2
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class DefaultProcessor extends AbstractProcessor {
/**
* 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);
}
}
LOGGER.config("Java Advanced Imaging: " + JAI.getBuildVersion() +
", TileCache capacity="+(float)(cache.getMemoryCapacity()/(1024*1024))+" 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,
LoggingKeys.EXCESSIVE_TILE_CACHE_$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>() {
public int compare(final String name1, final String name2) {
return name1.toLowerCase().compareTo(name2.toLowerCase());
}
};
/**
* 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<String,Operation>(COMPARATOR);
/**
* The rendering hints for JAI operations (never {@code null}).
* This field is usually given as argument to {@link OperationJAI} methods.
*/
private final Hints hints;
/**
* The service registry for finding {@link Operation} implementations.
*/
private final FactoryRegistry registry;
/**
* Constructs a default coverage processor. 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. 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 hints A set of additional rendering hints, or {@code null} if none.
*/
public DefaultProcessor(final RenderingHints hints) {
registry = new FactoryRegistry(Arrays.asList(new Class<?>[] {
Operation.class
}));
this.hints = new Hints(hints);
this.hints.put(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
this.hints.put(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.FALSE);
this.hints.put(Hints.GRID_COVERAGE_PROCESSOR, this); // Must overwrites user setting.
}
/**
* Sets the GRID_COVERAGE_PROCESSOR hint to the specified value.
* This is used by {@link BufferedProcessor} only.
*/
final void setProcessor(final AbstractProcessor processor) {
hints.put(Hints.GRID_COVERAGE_PROCESSOR, processor);
}
/**
* Removes the GRID_COVERAGE_PROCESSOR hint. The {@link AbstractOperation#getProcessor} method
* will automatically returns the default instance when this hint is not defined. Removing this
* hint provides three advantages for the common case when the default processor is used:
*
* <ul>
* <li>Avoid a strong reference to this processor in {@link RenderedImage} properties.</li>
* <li>Avoid serialization of this processor when a {@link RenderedImage} is serialized.</li>
* <li>Allows {@link AbstractOperation#getProcessor} to returns the {@link BufferedProcessor}
* instance instead of this instance.</li>
* </ul>
*/
@Override
void setAsDefault() {
hints.remove(Hints.GRID_COVERAGE_PROCESSOR);
}
/**
* Add 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();
}
addOperation0(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 addOperation0(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(
ErrorKeys.OPERATION_ALREADY_BOUND_$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.
*/
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.
*/
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(
ErrorKeys.OPERATION_NOT_FOUND_$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.
*/
public synchronized Coverage doOperation(final ParameterValueGroup parameters)
throws 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;
}
}
}
}
}
/*
* 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) {
final OperationNotFoundException exception = new OperationNotFoundException(
Errors.getResources(getLocale()).getString(
ErrorKeys.OPERATION_NOT_FOUND_$1, operationName));
exception.initCause(cause);
throw exception;
}
Coverage coverage = op.doOperation(parameters, hints);
if (interpolations != null && (coverage instanceof GridCoverage2D) &&
!(coverage instanceof Interpolator2D))
{
coverage = Interpolator2D.create((GridCoverage2D) coverage, interpolations);
}
log(source, coverage, operationName, false);
return coverage;
}
/**
* 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);
while (it.hasNext()) {
final Operation operation = it.next();
final String name = operation.getName().trim();
if (!operations.containsKey(name)) {
addOperation0(operation);
}
}
}
}