/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-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.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.TileCache;
import org.geotools.coverage.AbstractCoverage;
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.Arguments;
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.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.image.ImageUtilities;
import org.geotools.util.SoftValueHashMap;
import org.geotools.util.Utilities;
import org.geotools.util.logging.Logging;
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.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.util.InternationalString;
/**
* Base class for {@linkplain Coverage coverage} processor implementations.
*
* @since 2.2
*
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
* @author Simone Giannecchini, GeoSolutions S.A.S.
*/
public class CoverageProcessor {
/**
* The logger for coverage processing operations.
*/
public static final Logger LOGGER = Logging.getLogger("org.geotools.coverage.processing");
/**
* The logging level for reporting coverage operations.
* This level is equals or slightly lower than {@link Level#INFO}.
*/
public static final Level OPERATION = new LogLevel("OPERATION", 780);
/**
* 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 grid coverage logging level type.
*/
private static final class LogLevel extends Level {
/**
*
*/
private static final long serialVersionUID = 1L;
protected LogLevel(final String name, final int level) {
super(name, level);
}
}
/**
* The default coverage processor. Will be constructed only when first requested.
*
* @todo This is a temporary field, to be removed when a GeoAPI interfaces for coverage
* processing while be redesigned along the lines of ISO 19123.
*/
private static CoverageProcessor DEFAULT;
private final static SoftValueHashMap<Hints, CoverageProcessor> processorsPool= new SoftValueHashMap<Hints, CoverageProcessor>();
/**
* Cacheable instance of the {@link CoverageProcessor}. It prevents users to add operations manually calling {@link #addOperation(Operation)}.
*
* @author Simone Giannecchini, GeoSolutions S.A.S.
*
*/
private final static class CacheableCoverageProcessor extends CoverageProcessor {
public CacheableCoverageProcessor() {
super();
}
/**
* @param hints
*/
public CacheableCoverageProcessor(RenderingHints hints) {
super(hints);
}
@Override
protected void addOperation(Operation operation)
throws IllegalStateException {
throw new UnsupportedOperationException();
}
}
/**
* Augments the amount 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 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 = Collections.synchronizedMap(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.
*/
protected final Hints hints;
/**
* The service registry for finding {@link Operation} implementations.
*/
protected final FactoryRegistry registry;
/**
* Constructs a coverage processor.
*/
public CoverageProcessor() {
this(null);
}
/**
* 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 CoverageProcessor(final RenderingHints hints) {
registry = new FactoryRegistry(Arrays.asList(new Class<?>[] {
Operation.class
}));
this.hints = new Hints();
this.hints.put(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
this.hints.put(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.FALSE);
// override with user hints
if(hints!=null)
this.hints.add(hints);
}
/**
* Returns a default processor instance.
* <p>
* <strong>Note:</strong> this is a temporary method, until we have GeoAPI interface for
* coverage processor and a factory finder for their implementations.
*/
public static synchronized CoverageProcessor getInstance() {
return getInstance(null);
}
/**
* Returns a default processor instance.
* <p>
* <strong>Note:</strong> this is a temporary method, until we have GeoAPI interface for
* coverage processor and a factory finder for their implementations.
*/
public static synchronized CoverageProcessor getInstance(final Hints hints) {
if(hints==null||hints.isEmpty()){
if (DEFAULT == null) {
DEFAULT = new CacheableCoverageProcessor();
}
return DEFAULT;
}
if(processorsPool.containsKey(hints))
return processorsPool.get(hints);
final CoverageProcessor processor=new CacheableCoverageProcessor(hints);
processorsPool.put(hints, processor);
return processor;
}
/**
* The locale for logging message or reporting errors. The default implementations
* returns the {@linkplain Locale#getDefault default locale}. Subclasses can override
* this method if a different locale is wanted.
*/
public Locale getLocale() {
return Locale.getDefault();
}
/**
* Logs a message for an operation. The message will be logged only if the source grid
* coverage is different from the result (i.e. if the operation did some work).
*
* @param source The source grid coverage.
* @param result The resulting grid coverage.
* @param operationName the operation name.
* @param fromCache {@code true} if the result has been fetch from the cache.
*/
final void log(final Coverage source,
final Coverage result,
final String operationName,
final boolean fromCache)
{
if (source != result) {
String interp = "Nearest";
if (result instanceof Interpolator2D) {
interp = ImageUtilities.getInterpolationName(
((Interpolator2D) result).getInterpolation());
}
final Locale locale = getLocale();
final LogRecord record = Loggings.getResources(locale).getLogRecord(
OPERATION, LoggingKeys.APPLIED_OPERATION_$4,
getName((source!=null) ? source : result, locale),
operationName, interp, Integer.valueOf(fromCache ? 1 : 0));
// Note: DefaultProcessor is the class that will use this method.
record.setSourceClassName("org.geotools.coverage.processing.DefaultProcessor");
record.setSourceMethodName("doOperation");
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
}
}
/**
* Returns the primary source coverage from the specified parameters, or {@code null} if none.
*/
static Coverage getPrimarySource(final ParameterValueGroup parameters) {
try {
return (Coverage) parameters.parameter("Source").getValue();
} catch (ParameterNotFoundException exception) {
/*
* "Source" parameter may not exists. Conservatively
* assumes that the operation will do some usefull work.
*/
return null;
}
}
/**
* Returns the operation name for the specified parameters.
*/
static String getOperationName(final ParameterValueGroup parameters) {
return parameters.getDescriptor().getName().getCode().trim();
}
/**
* Returns the coverage name in the specified locale.
*/
private static String getName(final Coverage coverage, final Locale locale) {
if (coverage instanceof AbstractCoverage) {
final InternationalString name = ((AbstractCoverage) coverage).getName();
if (name != null) {
return name.toString(locale);
}
}
return Vocabulary.getResources(locale).getString(VocabularyKeys.UNTITLED);
}
/**
* Lists a summary of all operations to the specified stream.
*
* @param out The destination stream.
* @throws IOException if an error occured will writing to the stream.
*/
public void listOperations(final Writer out) throws IOException {
final Collection<Operation> operations = getOperations();
final CoverageParameterWriter writer = new CoverageParameterWriter(out);
final List<ParameterDescriptorGroup> descriptors = new ArrayList<ParameterDescriptorGroup>(operations.size());
for (final Iterator<Operation> it=operations.iterator(); it.hasNext();) {
final Operation operation = it.next();
if (operation instanceof AbstractOperation) {
descriptors.add(((AbstractOperation) operation).descriptor);
}
}
writer.summary(descriptors, null);
}
/**
* Prints a description of operations to the specified stream. If the {@code names} array
* is non-null, then only the specified operations are printed. Otherwise, all operations
* are printed. The description details include operation names and lists of parameters.
*
* @param out The destination stream.
* @param names The operation to print, or an empty array for none, or {@code null} for all.
* @throws IOException if an error occured will writing to the stream.
* @throws OperationNotFoundException if an operation named in {@code names} was not found.
*/
public void printOperations(final Writer out, final String[] names)
throws OperationNotFoundException, IOException
{
final CoverageParameterWriter writer = new CoverageParameterWriter(out);
final String lineSeparator = System.getProperty("line.separator", "\n");
if (names != null) {
for (int i=0; i<names.length; i++) {
final Operation operation = getOperation(names[i]);
if (operation instanceof AbstractOperation) {
out.write(lineSeparator);
writer.format(((AbstractOperation) operation).descriptor);
}
}
} else {
final Collection<Operation> operations = getOperations();
for (final Iterator<Operation> it=operations.iterator(); it.hasNext();) {
final Operation operation = it.next();
if (operation instanceof AbstractOperation) {
out.write(lineSeparator);
writer.format(((AbstractOperation) operation).descriptor);
}
}
}
}
/**
* 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 void addOperation(final Operation operation)
throws IllegalStateException {
Utilities.ensureNonNull("operation", operation);
synchronized (operations) {
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 Collection<Operation> getOperations() {
synchronized (operations) {
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 Operation getOperation(String name)
throws OperationNotFoundException {
Utilities.ensureNonNull("name", name);
name = name.trim();
synchronized (operations) {
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.
*/
@SuppressWarnings("unchecked")
public Coverage doOperation(final ParameterValueGroup parameters, final Hints hints){
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;
}
//set up hints
final Hints localMergeHints=this.hints.clone();
if(hints!=null)
localMergeHints.add(hints);
// processwith local hints
Coverage coverage = op.doOperation(parameters, localMergeHints);
if (interpolations != null && (coverage instanceof GridCoverage2D) &&
!(coverage instanceof Interpolator2D)){
coverage = Interpolator2D.create((GridCoverage2D) coverage, interpolations);
}
log(source, coverage, operationName, false);
return coverage;
}
/**
* 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 Coverage doOperation(final ParameterValueGroup parameters)
throws OperationNotFoundException {
return doOperation(parameters, null);
}
/**
* 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 void scanForPlugins() {
synchronized (operations) {
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);
}
}
}
}
/**
* Dumps to the {@linkplain System#out standard output stream} a list of operations for the
* default processor. If no argument is provided, then only a summary of operations is printed.
* If arguments are provided, then the operation parameters are printed for all operation names
* given in arguments. This method can been invoked from the command line. For example:
*
* <blockquote><pre>
* java org.geotools.coverage.processing.DefaultProcessor Interpolate
* </pre></blockquote>
*
* <strong>Note for Windows users:</strong> If the output contains strange
* symbols, try to supply an "{@code -encoding}" argument. Example:
*
* <blockquote><pre>
* java org.geotools.coverage.processing.DefaultProcessor -encoding Cp850
* </pre></blockquote>
*
* The codepage number (850 in the previous example) can be fetch from the DOS
* command line by entering the "{@code chcp}" command with no arguments.
*/
public static void main(String[] args) {
final Arguments arguments = new Arguments(args);
final boolean all = arguments.getFlag("-all");
args = arguments.getRemainingArguments(Integer.MAX_VALUE);
final CoverageProcessor processor = getInstance();
try {
if (args.length == 0) {
processor.listOperations(arguments.out);
} else {
processor.printOperations(arguments.out, all ? null : args);
}
} catch (OperationNotFoundException exception) {
arguments.out.println(exception.getLocalizedMessage());
} catch (IOException exception) {
// Should not occurs
exception.printStackTrace(arguments.out);
}
arguments.out.flush();
}
}