/*
* 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.renderer.lite.gridcoverage2d;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.ColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.ParameterBlock;
import java.awt.image.renderable.RenderedImageFactory;
import javax.media.jai.CRIFImpl;
import javax.media.jai.ImageLayout;
import javax.media.jai.JAI;
import javax.media.jai.OperationDescriptorImpl;
import javax.media.jai.PlanarImage;
import javax.media.jai.PointOpImage;
import javax.media.jai.iterator.RectIterFactory;
import javax.media.jai.iterator.WritableRectIter;
import javax.media.jai.registry.RenderedRegistryMode;
import javax.media.jai.util.ImagingException;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.image.TransfertRectIter;
import org.geotools.image.jai.Registry;
import org.geotools.referencing.piecewise.DefaultDomain1D;
import org.geotools.referencing.piecewise.Domain1D;
import org.geotools.referencing.piecewise.PiecewiseTransform1DElement;
import org.geotools.renderer.i18n.ErrorKeys;
import org.geotools.renderer.i18n.Errors;
/**
* Images are created using the {@code LinearClassifier.CRIF} inner class, where "CRIF" stands for {@link java.awt.image.renderable.ContextualRenderedImageFactory} . The image operation name is "org.geotools.RasterClassifier".
*
* @source $URL$
* @version $Id$
* @author Simone Giannecchini - GeoSolutions
* @since 2.4
*/
public class RasterClassifier extends PointOpImage {
/**
* The operation name.
*/
public static final String OPERATION_NAME = "org.geotools.RasterClassifier";
/**
* DomainElement1D lists for each bands. The array length must matches the number
* of bands in source image.
*/
private final ColorMapTransform<ColorMapTransformElement> pieces;
/**
* The index (zero based) of the band we are going to classify
*/
private int bandIndex;
/**
* Constructs a new {@code RasterClassifier}.
*
* @param image
* The source image.
* @param lic
* The category lists, one for each image's band.
* @param bandIndex
* @param hints
* The rendering hints.
*/
private RasterClassifier(final RenderedImage image,
final ColorMapTransform<ColorMapTransformElement> lic, int bandIndex,
final RenderingHints hints) {
super(image, prepareLayout(image, (ImageLayout) hints
.get(JAI.KEY_IMAGE_LAYOUT), lic), hints, false);
this.pieces = lic;
this.bandIndex = bandIndex;
permitInPlaceOperation();
}
/**
* Prepare the {@link ImageLayout} for the final image by building the
* {@link ColorModel} from the input
*
* @param image
* the image to classify.
* @param layout
* a proposed layout.
* @param lic
* the pieces we are asked to use.
* @return a layout suitable for the image that we'll create after this
*/
private static ImageLayout prepareLayout(RenderedImage image,
ImageLayout layout, ColorMapTransform<ColorMapTransformElement> lic) {
// //
//
// Get the final color model from the pieces and from that one
// create the sample model
//
// ///
final ColorModel finalColorModel = lic.getColorModel();
// create a good sample model for the output raster
final SampleModel finalSampleModel = lic.getSampleModel(image
.getWidth(), image.getHeight());
if (layout == null)
layout = new ImageLayout();
layout.setColorModel(finalColorModel);
layout.setSampleModel(finalSampleModel);
return layout;
}
/**
* Computes one of the destination image tile.
*
* @todo There are two optimisations we could do here:
* <ul>
* <li>If source and destination are the same raster, then a single
* {@link WritableRectIter} object would be more efficient (the hard
* work is to detect if source and destination are the same).</li>
* <li>If the destination image is a single-banded, non-interleaved
* sample model, we could apply the transform directly in the
* {@link java.awt.image.DataBuffer}. We can even avoid to copy
* sample value if source and destination raster are the same.</li>
* </ul>
*
* @param sources
* An array of length 1 with source image.
* @param dest
* The destination tile.
* @param destRect
* the rectangle within the destination to be written.
*/
protected void computeRect(final PlanarImage[] sources,
final WritableRaster dest, final Rectangle destRect) {
final PlanarImage source = sources[0];
WritableRectIter iterator = RectIterFactory.createWritable(dest,
destRect);
if (true) {
// TODO: Detect if source and destination rasters are the same. If
// they are the same, we should skip this block. Iteration will then
// be faster.
iterator = TransfertRectIter.create(RectIterFactory.create(source,
destRect), iterator);
}
// ////////////////////////////////////////////////////////////////////
//
// Prepare the iterator to work on the correct bands, if this is
// requested.
//
// ////////////////////////////////////////////////////////////////////
if (!iterator.finishedBands()) {
for (int i = 0; i < bandIndex; i++)
iterator.nextBand();
}
// ////////////////////////////////////////////////////////////////////
//
// Check if we can make good use of a no data category for filling gaps
// in the input range
//
// ////////////////////////////////////////////////////////////////////
double gapsValue = Double.NaN;
boolean hasGapsValue = false;
if (this.pieces.hasDefaultValue()) {
gapsValue = this.pieces.getDefaultValue();
hasGapsValue = true;
}
// ////////////////////////////////////////////////////////////////////
//
// Check if we can optimize this operation by reusing the last used
// category first. The speed up we get can be substantial since we avoid
// n explicit search in the category list for the fitting category given
// a certain sample value.
//
// This is not possible when the NoDataCategories range overlaps with
// the range of the valid values. In this case we have ALWAYS to check
// first the NoDataRange when applying transformations. If we optimized
// in this case we would get erroneous results given to the fact that we
// might be reusing a valid sample category while we should be using a
// no data one.
//
// ////////////////////////////////////////////////////////////////////
PiecewiseTransform1DElement last = null;
final boolean useLast = pieces instanceof DefaultDomain1D;
do {
try {
iterator.startLines();
if (!iterator.finishedLines())
do {
iterator.startPixels();
if (!iterator.finishedPixels())
do {
// //
//
// get the input value to be transformed
//
// //
final double value = iterator.getSampleDouble();
// //
//
// get the correct category for this
// transformation
//
// //
final PiecewiseTransform1DElement transform;
if (useLast) {
if (last != null && last.contains(value))
transform = last;
else {
last = transform = pieces.findDomainElement(value);
}
} else
transform = (PiecewiseTransform1DElement) pieces.findDomainElement(value);
// //
//
// in case everything went fine let's apply the
// transform.
//
// //
if (transform != null)
iterator.setSample(transform.transform(value));
else {
// //
//
// if we did not find one let's try to use
// one of the nodata ones to fill the gaps,
// if we are allowed to (see above).
//
// //
if (hasGapsValue)
iterator.setSample(gapsValue);
else
// //
//
// if we did not find one let's throw a
// nice error message
//
// //
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$1, Double.toString(value)));
}
} while (!iterator.nextPixelDone());
} while (!iterator.nextLineDone());
} catch (Throwable cause) {
throw new ImagingException(
cause.getLocalizedMessage(),cause);
}
if (bandIndex != -1)
break;
} while (iterator.finishedBands());
}
// ///////////////////////////////////////////////////////////////////////////////
// ////// ////////
// ////// REGISTRATION OF "SampleTranscode" IMAGE OPERATION ////////
// ////// ////////
// ///////////////////////////////////////////////////////////////////////////////
/**
* The operation descriptor for the "SampleTranscode" operation. This
* operation can apply the
* {@link GridSampleDimension#getSampleToGeophysics sampleToGeophysics}
* transform on all pixels in all bands of an image. The transformations are
* supplied as a list of {@link GridSampleDimension}s, one for each band.
* The supplied {@code GridSampleDimension} objects describe the pieces
* in the <strong>source</strong> image. The target image will matches
* sample dimension
*
* <code>{@link GridSampleDimension#geophysics geophysics}(!isGeophysics)</code>,
*
* where {@code isGeophysics} is the previous state of the sample dimension.
*/
private static final class Descriptor extends OperationDescriptorImpl {
/**
*
*/
private static final long serialVersionUID = 7954257625240335874L;
/**
* Construct the descriptor.
*/
public Descriptor() {
super(
new String[][] {
{ "GlobalName", OPERATION_NAME },
{ "LocalName", OPERATION_NAME },
{ "Vendor", "Geotools 2" },
{ "Description",
"Transformation from sample to geophysics values" },
{ "DocURL", "http://www.geotools.org/" },
{ "Version", "1.0" } },
new String[] { RenderedRegistryMode.MODE_NAME }, 1,
new String[] { "Domain1D", "bandIndex" }, // Argument
// names
new Class[] { ColorMapTransform.class,
Integer.class }, // Argument
// classes
new Object[] { NO_PARAMETER_DEFAULT, Integer.valueOf(-1) }, // Default
// values
// for parameters,
null // No restriction on valid parameter values.
);
}
/**
* Returns {@code true} if the parameters are valids. This
* implementation check that the number of bands in the source image is
* equals to the number of supplied sample dimensions, and that all
* sample dimensions has pieces.
*
* @param modeName
* The mode name (usually "Rendered").
* @param param
* The parameter block for the operation to performs.
* @param message
* A buffer for formatting an error message if any.
*/
protected boolean validateParameters(final String modeName,
final ParameterBlock param, final StringBuffer message) {
if (!super.validateParameters(modeName, param, message)) {
return false;
}
final RenderedImage source = (RenderedImage) param.getSource(0);
final Domain1D lic = (Domain1D) param.getObjectParameter(0);
if (lic == null)
return false;
final int numBands = source.getSampleModel().getNumBands();
final int bandIndex = param.getIntParameter(1);
if (bandIndex == -1)
return true;
if (bandIndex < 0 || bandIndex >= numBands) {
return false;
}
return true;
}
}
/**
* The {@link RenderedImageFactory} for the "SampleTranscode" operation.
*/
private static final class CRIF extends CRIFImpl {
/**
* Creates a {@link RenderedImage} representing the results of an
* imaging operation for a given {@link ParameterBlock} and
* {@link RenderingHints}.
*/
public RenderedImage create(final ParameterBlock param,
final RenderingHints hints) {
final RenderedImage image = (RenderedImage) param.getSource(0);
final ColorMapTransform<ColorMapTransformElement> lic = (ColorMapTransform<ColorMapTransformElement>) param.getObjectParameter(0);
final int bandIndex = param.getIntParameter(1);
return new RasterClassifier(image, lic, bandIndex, hints);
}
}
/**
* Register the RasterClassifier operation to the operation registry of the
* specified JAI instance. This method is invoked by the static initializer
* of {@link GridSampleDimension}.
*
* @param jai
* JAI instance in which we want to register the RasterClassifier
* operation.
* @return <code>true</code> if everything goes fine, <code>false</code>
* otherwise.
*/
public static boolean register(final JAI jai) {
return Registry.registerRIF(jai, new Descriptor(), OPERATION_NAME,
new CRIF());
}
}