/* * 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.grid; import java.awt.Point; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.WritableRenderedImage; import java.awt.image.renderable.RenderableImage; import java.io.IOException; import java.io.InvalidClassException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.LogRecord; import javax.measure.unit.Unit; import javax.media.jai.Interpolation; import javax.media.jai.OperationNode; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedImageAdapter; import javax.media.jai.remote.SerializableRenderedImage; import org.geotools.coverage.AbstractCoverage; import org.geotools.coverage.GridSampleDimension; import org.geotools.factory.Hints; import org.geotools.geometry.Envelope2D; import org.geotools.resources.Classes; import org.geotools.resources.coverage.CoverageUtilities; 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.CannotEvaluateException; import org.opengis.coverage.PointOutsideCoverageException; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.PixelInCell; /** * Basic access to grid data values backed by a two-dimensional * {@linkplain RenderedImage rendered image}. Each band in an image is represented as a * {@linkplain GridSampleDimension sample dimension}. *
* Grid coverages are usually two-dimensional. However, {@linkplain #getEnvelope their envelope} * may have more than two dimensions. For example, a remote sensing image may be valid only over * some time range (the time of satellite pass over the observed area). Envelopes for such grid * coverage can have three dimensions: the two usual ones (horizontal extent along x * and y), and a third one for start time and end time (time extent along t). * However, the {@linkplain GeneralGridRange grid range} for all extra-dimension must * have a {@linkplain GeneralGridRange#getLength size} not greater than 1. In other words, a * {@code GridCoverage2D} can be a slice in a 3 dimensional grid coverage. Each slice can have an * arbitrary width and height (like any two-dimensional images), but only 1 voxel depth (a "voxel" * is a three-dimensional pixel). *
* Serialization note:
* Because it is serializable, {@code GridCoverage2D} can be included as method argument or as
* return type in Remote Method Invocation (RMI). However, the pixel data are not
* sent during serialization. Instead, the image data are transmitted "on-demand" using socket
* communications. This mechanism is implemented using JAI {@link SerializableRenderedImage}
* class. While serialization (usually on server side) should work on J2SE 1.4 and above,
* deserialization (usually on client side) of {@code GridCoverage2D} instances requires J2SE 1.5.
*
* @since 2.1
*
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class GridCoverage2D extends AbstractGridCoverage {
/**
* For compatibility during cross-version serialization.
*/
private static final long serialVersionUID = 667472989475027853L;
/**
* Whatever default grid range computation should be performed on transform
* relative to pixel center or relative to pixel corner. The former is OGC
* convention while the later is Java convention.
*/
private static final PixelInCell PIXEL_IN_CELL = PixelInCell.CELL_CORNER;
/**
* The raster data.
*/
protected transient final PlanarImage image;
/**
* The serialized image, as an instance of {@link SerializableRenderedImage}.
* This image will be created only when first needed during serialization.
*/
private RenderedImage serializedImage;
/**
* The grid geometry.
*/
protected final GridGeometry2D gridGeometry;
/**
* List of sample dimension information for the grid coverage.
* For a grid coverage, a sample dimension is a band. The sample dimension information
* include such things as description, data type of the value (bit, byte, integer...),
* the no data values, minimum and maximum values and a color table if one is associated
* with the dimension. A coverage must have at least one sample dimension.
*
* The content of this array should never be modified.
*/
final GridSampleDimension[] sampleDimensions;
/**
* The views returned by {@link #views}. Constructed when first needed.
* Note that some views may appear in the {@link #sources} list.
*/
private transient ViewsManager views;
/**
* The set of views that this coverage represents. Will be created
* by {@link #getViewTypes} only when first needed.
*/
private transient Set
* This constructor accepts an optional set of properties. Keys are {@link String} objects
* ({@link javax.media.jai.util.CaselessStringKey} are accepted as well), while values may
* be any {@link Object}.
*
* @param name
* The grid coverage name.
* @param image
* The image.
* @param gridGeometry
* The grid geometry (must contains an {@linkplain GridGeometry2D#getEnvelope envelope}
* with its {@linkplain GridGeometry2D#getCoordinateReferenceSystem coordinate reference
* system} and a "{@linkplain GridGeometry2D#getGridToCoordinateSystem grid to CRS}"
* transform).
* @param bands
* Sample dimensions for each image band, or {@code null} for default sample dimensions.
* If non-null, then this array's length must matches the number of bands in {@code image}.
* @param sources
* The sources for this grid coverage, or {@code null} if none.
* @param properties
* The set of properties for this coverage, or {@code null} none.
* @param hints
* An optional set of hints, or {@code null} if none.
* @throws IllegalArgumentException
* If the number of bands differs from the number of sample dimensions.
*
* @since 2.5
*/
protected GridCoverage2D(final CharSequence name,
final PlanarImage image,
GridGeometry2D gridGeometry,
final GridSampleDimension[] bands,
final GridCoverage[] sources,
final Map,?> properties,
final Hints hints)
throws IllegalArgumentException
{
super(name, gridGeometry.getCoordinateReferenceSystem(), sources, image, properties);
this.image = image;
/*
* Wraps the user-supplied sample dimensions into instances of RenderedSampleDimension. This
* process will creates default sample dimensions if the user supplied null values. Those
* default will be inferred from image type (integers, floats...) and range of values. If
* an inconsistency is found in user-supplied sample dimensions, an IllegalArgumentException
* is thrown.
*/
sampleDimensions = new GridSampleDimension[image.getNumBands()];
RenderedSampleDimension.create(name, image, bands, sampleDimensions);
/*
* Computes the grid range if it was not explicitly provided. The range will be inferred
* from the image size, if needed. The envelope computation (if needed) requires a valid
* 'gridToCRS' transform in the GridGeometry object. In any case, the envelope must be
* non-empty and its dimension must matches the coordinate reference system's dimension.
*/
final int dimension = crs.getCoordinateSystem().getDimension();
if (!gridGeometry.isDefined(GridGeometry2D.GRID_RANGE_BITMASK)) {
final GridEnvelope r = new GeneralGridEnvelope(image, dimension);
if (gridGeometry.isDefined(GridGeometry2D.GRID_TO_CRS_BITMASK)) {
gridGeometry = new GridGeometry2D(r, PIXEL_IN_CELL,
gridGeometry.getGridToCRS(PIXEL_IN_CELL), crs, hints);
} else {
/*
* If the math transform was not explicitly specified by the user, then it will be
* computed from the envelope. In this case, some heuristic rules are used in order
* to decide if we should reverse some axis directions or swap axis.
*/
gridGeometry = new GridGeometry2D(r, gridGeometry.getEnvelope());
}
} else {
/*
* Makes sure that the 'gridToCRS' transform is defined.
* An exception will be thrown otherwise.
*/
gridGeometry.getGridToCRS();
}
this.gridGeometry = gridGeometry;
assert gridGeometry.isDefined(GridGeometry2D.CRS_BITMASK |
GridGeometry2D.ENVELOPE_BITMASK |
GridGeometry2D.GRID_RANGE_BITMASK |
GridGeometry2D.GRID_TO_CRS_BITMASK);
/*
* Last argument checks. The image size must be consistent with the grid range
* and the envelope must be non-empty.
*/
final String error = GridGeometry2D.checkConsistency(image, gridGeometry);
if (error != null) {
throw new IllegalArgumentException(error);
}
if (dimension <= Math.max(gridGeometry.axisDimensionX, gridGeometry.axisDimensionY)
|| !(gridGeometry.envelope.getSpan(gridGeometry.axisDimensionX) > 0)
|| !(gridGeometry.envelope.getSpan(gridGeometry.axisDimensionY) > 0))
{
throw new IllegalArgumentException(Errors.format(ErrorKeys.EMPTY_ENVELOPE));
}
}
/**
* Returns {@code true} if grid data can be edited. The default
* implementation returns {@code true} if {@link #image} is an
* instance of {@link WritableRenderedImage}.
*/
@Override
public boolean isDataEditable() {
return (image instanceof WritableRenderedImage);
}
/**
* Returns information for the grid coverage geometry. Grid geometry
* includes the valid range of grid coordinates and the georeferencing.
*/
public GridGeometry2D getGridGeometry() {
final String error = GridGeometry2D.checkConsistency(image, gridGeometry);
if (error != null) {
throw new IllegalStateException(error);
}
return gridGeometry;
}
/**
* Returns the bounding box for the coverage domain in coordinate reference system coordinates.
* The returned envelope have at least two dimensions. It may have more dimensions if the
* coverage has some extent in other dimensions (for example a depth, or a start and end time).
*/
@Override
public Envelope getEnvelope() {
return gridGeometry.getEnvelope();
}
/**
* Returns the two-dimensional bounding box for the coverage domain in coordinate reference
* system coordinates. If the coverage envelope has more than two dimensions, only the
* dimensions used in the underlying rendered image are returned.
*
* @return The two-dimensional bounding box.
*/
public Envelope2D getEnvelope2D() {
return gridGeometry.getEnvelope2D();
}
/**
* Returns the two-dimensional part of this grid coverage CRS. If the
* {@linkplain #getCoordinateReferenceSystem complete CRS} is two-dimensional, then this
* method returns the same CRS. Otherwise it returns a CRS for the two first axis having
* a {@linkplain GridRange#length length} greater than 1 in the grid range. Note that those
* axis are garanteed to appears in the same order than in the complete CRS.
*
* @return The two-dimensional part of the grid coverage CRS.
*
* @see #getCoordinateReferenceSystem
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem2D() {
return gridGeometry.getCoordinateReferenceSystem2D();
}
/**
* Returns the number of bands in the grid coverage.
*/
public int getNumSampleDimensions() {
return sampleDimensions.length;
}
/**
* Retrieve sample dimension information for the coverage.
* For a grid coverage, a sample dimension is a band. The sample dimension information
* include such things as description, data type of the value (bit, byte, integer...),
* the no data values, minimum and maximum values and a color table if one is associated
* with the dimension. A coverage must have at least one sample dimension.
*/
public GridSampleDimension getSampleDimension(final int index) {
return sampleDimensions[index];
}
/**
* Returns all sample dimensions for this grid coverage.
*
* @return All sample dimensions.
*/
public GridSampleDimension[] getSampleDimensions() {
return sampleDimensions.clone();
}
/**
* Returns the interpolation used for all {@code evaluate(...)} methods.
* The default implementation returns {@link javax.media.jai.InterpolationNearest}.
*
* @return The interpolation.
*/
public Interpolation getInterpolation() {
return Interpolation.getInstance(Interpolation.INTERP_NEAREST);
}
/**
* Returns the value vector for a given location (world coordinates).
* A value for each sample dimension is included in the vector.
*/
public Object evaluate(final DirectPosition point) throws CannotEvaluateException {
final int dataType = image.getSampleModel().getDataType();
switch (dataType) {
case DataBuffer.TYPE_BYTE: return evaluate(point, (byte []) null);
case DataBuffer.TYPE_SHORT: // Fall through
case DataBuffer.TYPE_USHORT: // Fall through
case DataBuffer.TYPE_INT: return evaluate(point, (int []) null);
case DataBuffer.TYPE_FLOAT: return evaluate(point, (float []) null);
case DataBuffer.TYPE_DOUBLE: return evaluate(point, (double[]) null);
default: throw new CannotEvaluateException();
}
}
/**
* Returns a sequence of byte values for a given location (world coordinates).
*
* @param coord World coordinates of the location to evaluate.
* @param dest An array in which to store values, or {@code null}.
* @return An array containing values.
* @throws CannotEvaluateException if the values can't be computed at the specified coordinate.
* More specifically, {@link PointOutsideCoverageException} is thrown if the evaluation
* failed because the input point has invalid coordinates.
*/
@Override
public byte[] evaluate(final DirectPosition coord, byte[] dest)
throws CannotEvaluateException
{
final int[] array = evaluate(coord, (int[]) null);
if (dest == null) {
dest = new byte[array.length];
}
for (int i=0; i
* {@link ViewType#GEOPHYSICS GEOPHYSICS}: all sample values are equals to geophysics
* ("real world") values without the need for any transformation. The
* {@linkplain SampleDimension#getSampleToGeophysics sample to geophysics} transform
* {@linkplain org.opengis.referencing.operation.MathTransform1D#isIdentity is identity}
* for all sample dimensions. "No data" values (if any) are expressed as
* {@linkplain Float#NaN NaN} numbers. This view is suitable for computation, but usually
* not for rendering.
*
* {@link ViewType#PACKED PACKED}: sample values are typically integers. A
* {@linkplain SampleDimension#getSampleToGeophysics sample to geophysics} transform may
* exists for converting them to "real world" values.
*
* {@link ViewType#RENDERED RENDERED}: synonymous of {@code PACKED} for now. Will be
* improved in a future version.
*
* {@link ViewType#PHOTOGRAPHIC PHOTOGRAPHIC}: synonymous of {@code RENDERED} for now.
* Will be improved in a future version.
*
* {@link ViewType#SAME SAME}: returns {@code this} coverage unchanged.
*
* This method is defined here for {@link ViewsManager} needs, which invokes it. But it
* make sense only for {@link Calculator2D}, which override it with protected access.
* For other subclasses, we do not allow overriding (i.e. we keep this method package-
* privated) on purpose. See {@link #getViewClass} for the reason.
*/
GridCoverage2D specialize(final GridCoverage2D view) {
return view;
}
/**
* Returns the base class of the view returned by {@link #specialize}, or {@code null} if
* unknown. This method is invoked by {@link ViewsManager#create} in order to determine
* if a given coverage can share its views with an other coverage. The condition tested
* by {@link ViewsManager} (namely: coverages have the same image, same grid geometry and
* same sample dimensions) are suffisient only if the coverages build the views in the same
* way. The last condition can be garantee only if we know how {@link #specialize} is
* implemented. It is safe for non-{@link Calculator2D} classes (because users can not
* override {@link #specialize} and for final classes like {@link Interpolator2D}, but
* the later must returns a different class in order to tells {@link ViewsManager} that
* it does not build the views in the same way.
*/
Class extends GridCoverage2D> getViewClass() {
return GridCoverage2D.class;
}
/**
* Copies the views from this class into the specified coverage and returns them. The views
* are actually shared, i.e. views created for one coverage can be used by the other. This
* method is for internal use by {@link ViewsManager} only.
*/
final synchronized ViewsManager copyViewsTo(final GridCoverage2D target) {
if (views == null) {
views = ViewsManager.create(this);
}
if (target.views == null) {
target.views = views;
} else if (target.views != views) {
throw new IllegalStateException(); // As a safety, but should never happen.
}
return views;
}
/**
* Returns the set of views that this coverage represents. The same coverage may be used for
* more than one view. For example a coverage could be valid both as a {@link ViewType#PACKED
* PACKED} and {@link ViewType#RENDERED RENDERED} view.
*
* @return The set of views that this coverage represents.
*
* @since 2.5
*/
public synchronized Set
*
* This safety check helps to prevent the disposal of an {@linkplain #image} that still
* used in a JAI operation chain. It doesn't prevent the disposal in every cases however.
* When unsure about whatever a coverage is still in use or not, it is safer to not invoke
* this method and rely on the garbage collector instead.
*
* @see PlanarImage#dispose
*
* @since 2.4
*/
@Override
public synchronized boolean dispose(final boolean force) {
if (views != null) {
if (views.dispose(force).contains(this)) {
// The remaining GridCoverage2D include this one,
// which means that this view has not been disposed.
return false;
}
views = null;
} else if (!disposeImage(force)) {
return false;
}
return super.dispose(force);
}
/**
* Disposes only the {@linkplain #image}, not the views. This method is invoked by
* {@link ViewsManager#dispose}. This method checks the set of every sinks,
* which may or may not be {@link RenderedImage}s. If there is no sinks, we can process.
*/
final synchronized boolean disposeImage(final boolean force) {
if (!force) {
Collection> sinks = image.getSinks();
if (sinks != null && !sinks.isEmpty()) {
return false;
}
}
image.dispose();
return true;
}
/**
* Returns a string representation of this grid coverage.
* This is mostly for debugging purpose and may change in any future version.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(super.toString());
final String lineSeparator = System.getProperty("line.separator", "\n");
buffer.append("\u2514 Image=").append(Classes.getShortClassName(image)).append('[');
if (image instanceof OperationNode) {
buffer.append('"').append(((OperationNode) image).getOperationName()).append('"');
}
buffer.append(']');
if (views == null || !Thread.holdsLock(views)) {
/*
* We use Thread.holdsLock(views) as a semaphore for avoiding never-ending loop if
* toString() is invoked from ViewsManager (either by IDE debugger or by 'println'
* statement). Because ViewsManager is not public, this trick doesn't impact users.
*/
buffer.append(" as views ").append(getViewTypes());
}
return buffer.append(lineSeparator).toString();
}
}
* @param coord grid (ie. pixel) coordinates
* @param dest an optionally pre-allocated array; if non-null, its length should be
* equal to the number of bands (sample dimensions)
*
* @return band values for the given grid (pixel) location
*
* @throws PointOutsideCoverageException if the supplied coords are outside the
* grid bounds
*/
public int[] evaluate(final GridCoordinates2D coord, final int[] dest) {
if (image.getBounds().contains(coord.x, coord.y)) {
return image.getTile(image.XToTileX(coord.x), image.YToTileY(coord.y)).getPixel(coord.x, coord.y, dest);
}
throw new PointOutsideCoverageException(formatEvaluateError(coord, true));
}
/**
* Return sample dimension (band) values as an array of floats for the given
* grid location. The range of valid grid coordinates can be retrieved as
* in this example:
*
* GridEnvelope2D gridBounds = coverage.getGridGeometry2D().getGridRange();
*
* @param coord grid (ie. pixel) coordinates
* @param dest an optionally pre-allocated array; if non-null, its length should be
* equal to the number of bands (sample dimensions)
*
* @return band values for the given grid (pixel) location
*
* @throws PointOutsideCoverageException if the supplied coords are outside the
* grid bounds
*/
public float[] evaluate(final GridCoordinates2D coord, final float[] dest) {
if (image.getBounds().contains(coord.x, coord.y)) {
return image.getTile(image.XToTileX(coord.x), image.YToTileY(coord.y)).getPixel(coord.x, coord.y, dest);
}
throw new PointOutsideCoverageException(formatEvaluateError(coord, true));
}
/**
* Return sample dimension (band) values as an array of doubles for the given
* grid location. The range of valid grid coordinates can be retrieved as
* in this example:
*
* GridEnvelope2D gridBounds = coverage.getGridGeometry2D().getGridRange();
*
* @param coord grid (ie. pixel) coordinates
* @param dest an optionally pre-allocated array; if non-null, its length should be
* equal to the number of bands (sample dimensions)
*
* @return band values for the given grid (pixel) location
*
* @throws PointOutsideCoverageException if the supplied coords are outside the
* grid bounds
*/
public double[] evaluate(final GridCoordinates2D coord, final double[] dest) {
if (image.getBounds().contains(coord.x, coord.y)) {
return image.getTile(image.XToTileX(coord.x), image.YToTileY(coord.y)).getPixel(coord.x, coord.y, dest);
}
throw new PointOutsideCoverageException(formatEvaluateError(coord, true));
}
/**
* Returns a debug string for the specified coordinate. This method produces a
* string with pixel coordinates and pixel values for all bands (with geophysics
* values or category name in parenthesis). Example for a 1-banded image:
*
*
* GridEnvelope2D gridBounds = coverage.getGridGeometry2D().getGridRange();
*
*
* @param coord The coordinate point where to evaluate.
* @return A string with pixel coordinates and pixel values at the specified location,
* or {@code null} if {@code coord} is outside coverage.
*/
public synchronized String getDebugString(final DirectPosition coord) {
Point2D pixel = gridGeometry.toPoint2D(coord);
pixel = gridGeometry.inverseTransform(pixel);
final int x = (int)Math.round(pixel.getX());
final int y = (int)Math.round(pixel.getY());
if (image.getBounds().contains(x,y)) { // getBounds() returns a cached instance.
final int numBands = image.getNumBands();
final Raster raster = image.getTile(image.XToTileX(x), image.YToTileY(y));
final int datatype = image.getSampleModel().getDataType();
final StringBuilder buffer = new StringBuilder();
buffer.append('(').append(x).append(',').append(y).append(")=[");
for (int band=0; band(1171,1566)=[196 (29.6 °C)]
*
*
* This method may be understood as applying the JAI's
* {@linkplain javax.media.jai.operator.PiecewiseDescriptor piecewise} operation with
* breakpoints specified by the {@link org.geotools.coverage.Category} objects in each
* sample dimension. However, it is more general in that the transformation specified
* with each breakpoint doesn't need to be linear. On an implementation note, this method
* tries to use the first of the following operations which is found applicable:
* identity,
* {@linkplain javax.media.jai.operator.LookupDescriptor lookup},
* {@linkplain javax.media.jai.operator.RescaleDescriptor rescale},
* {@linkplain javax.media.jai.operator.PiecewiseDescriptor piecewise} and in
* last ressort a more general (but slower) sample transcoding algorithm.
*
* @param type The kind of view wanted.
* @return The grid coverage. Never {@code null}, but may be {@code this}.
*
* @see GridSampleDimension#geophysics
* @see org.geotools.coverage.Category#geophysics
* @see javax.media.jai.operator.LookupDescriptor
* @see javax.media.jai.operator.RescaleDescriptor
* @see javax.media.jai.operator.PiecewiseDescriptor
*
* @since 2.5
*/
public GridCoverage2D view(final ViewType type) {
if (ViewType.SAME.equals(type)) {
return this;
}
synchronized (this) {
if (views == null) {
views = ViewsManager.create(this);
}
}
// Do not synchronize past this point, because ViewsManager.get is already
// synchronized. We need to rely on ViewsManager locking because the views
// are shared among many GridCoverage2D instances.
final Hints hints = null; // We may revisit that later.
return views.get(this, type, hints);
}
/**
* Returns the native view to be given to a newly created {@link ViewsManager}. For
* {@link GridCoverage2D}, this is always {@code this} because the first coverage to
* instantiate a {@link ViewsManager} can not be anything else than native, since the
* views do not exist yet. For {@link Calculator2D} (which is a decorator around an
* other {@link GridCoverage2D}), we use the native view of its source.
*/
GridCoverage2D getNativeView() {
return this;
}
/**
* Invoked (indirectly) by {@linkplain #view view}(type)
when the
* {@linkplain ViewType#PACKED packed}, {@linkplain ViewType#GEOPHYSICS geophysics} or
* {@linkplain ViewType#PHOTOGRAPHIC photographic} view of this grid coverage needs to
* be created.
*
*
*