/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2003-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;
import java.util.Date;
import java.awt.geom.Point2D;
import java.awt.geom.Dimension2D;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import org.opengis.coverage.Coverage;
import org.opengis.coverage.SampleDimension;
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.coverage.PointOutsideCoverageException;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.util.InternationalString;
import org.geotoolkit.lang.Decorator;
import org.geotoolkit.util.DateRange;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.grid.GridCoverageFactory;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.crs.DefaultGeographicCRS;
import org.apache.sis.referencing.crs.DefaultTemporalCRS;
import org.geotoolkit.referencing.operation.MathTransforms;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.internal.metadata.AxisDirections;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.resources.Errors;
/**
* Convenience view of an other coverage with <var>x</var>, <var>y</var> and <var>time</var> axis.
* This class provides {@code evaluate} methods in two versions: the usual one expecting a complete
* {@linkplain DirectPosition direct position}, and an other one expecting the {@linkplain Point2D
* spatial position} and the {@linkplain Date date} as separated arguments. This class will detect
* by itself which dimension is the time axis. It will also tries to uses the {@code Point2D}
* {@linkplain java.awt.geom.Point2D.Double#x x} value for {@linkplain AxisDirection#EAST east}
* or west direction, and the {@linkplain java.awt.geom.Point2D.Double#y y} value for
* {@linkplain AxisDirection#NORTH north} or south direction. The dimension mapping can be
* examined with the {@link #toSourceDimension} method.
*
* {@section Synchronization}
* This class is not thread safe for performance reasons. If desired, users should create one
* instance of {@code SpatioTemporalCoverage3D} for each thread.
*
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @since 2.1
* @module
*/
@Decorator(Coverage.class)
public class SpatioTemporalCoverage3D extends AbstractCoverage {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 2885506902340338431L;
/**
* The hints for fetching factories.
*/
private static final Hints HINTS = null;
/**
* A set of usual axis directions for <var>x</var> and <var>y</var> values (opposite directions
* not required). If an ordinate value is oriented toward one of those directions, it will be
* interpreted as the {@link java.awt.geom.Point2D.Double#x} value if the direction was found at
* an even index, or as the {@link java.awt.geom.Point2D.Double#y} value if the direction was
* found at an odd index.
*/
private static final AxisDirection[] DIRECTIONS = {
AxisDirection.EAST, AxisDirection.NORTH,
AxisDirection.DISPLAY_RIGHT, AxisDirection.DISPLAY_DOWN,
AxisDirection.COLUMN_POSITIVE, AxisDirection.ROW_POSITIVE
};
/**
* The wrapped coverage.
*/
protected final Coverage coverage;
/**
* The temporal coordinate system, as a Geotk implementation in order to gets the
* {@link DefaultTemporalCRS#toDate} and {@link DefaultTemporalCRS#toValue} methods.
*/
protected final DefaultTemporalCRS temporalCRS;
/**
* The dimension of the temporal coordinate system.
* All other dimensions are expected to be the spatial ones.
*/
protected final int temporalDimension;
/**
* The dimension for <var>x</var> and <var>y</var> coordinates.
*/
protected final int xDimension, yDimension;
/**
* The geographic bounding box. Will be computed only when first needed.
*/
private transient GeographicBoundingBox boundingBox;
/**
* The direct position to uses for {@code evaluate(...)} methods.
* This object is cached and reused for performance purpose. However,
* this caching sacrifies {@code SpatioTemporalCoverage3D} thread safety.
*/
private final GeneralDirectPosition coordinate;
/**
* The grid coverage factory for {@link #getCoverage2D} method.
* Will be created only when first needed.
*/
private transient GridCoverageFactory factory;
/**
* Constructs a new coverage. The coordinate reference system will be the same than the
* wrapped coverage, which must be three dimensional. This CRS must have a
* {@linkplain DefaultTemporalCRS temporal} component.
*
* @param name The name for this coverage, or {@code null} for the same than {@code coverage}.
* @param coverage The source coverage.
* @throws IllegalArgumentException if the coverage CRS doesn't have a temporal component.
*/
public SpatioTemporalCoverage3D(final CharSequence name, final Coverage coverage)
throws IllegalArgumentException
{
super(name, coverage);
final CoordinateSystem cs = crs.getCoordinateSystem();
final int dimension = cs.getDimension();
if (dimension != 3) {
throw new MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_2, 3, dimension));
}
if (coverage instanceof SpatioTemporalCoverage3D) {
final SpatioTemporalCoverage3D source = (SpatioTemporalCoverage3D) coverage;
this.coverage = source.coverage;
this.temporalCRS = source.temporalCRS;
this.temporalDimension = source.temporalDimension;
this.xDimension = source.xDimension;
this.yDimension = source.yDimension;
this.boundingBox = source.boundingBox;
} else {
this.coverage = coverage;
temporalCRS = DefaultTemporalCRS.castOrCopy(CRS.getTemporalComponent(crs));
if (temporalCRS == null) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalCoordinateReferenceSystem));
}
temporalDimension = CRSUtilities.getDimensionOf(crs, temporalCRS.getClass());
final int xDimension = (temporalDimension!=0) ? 0 : 1;
final int yDimension = (temporalDimension!=2) ? 2 : 1;
Boolean swap = null; // 'null' if unknown, otherwise TRUE or FALSE.
control: for (int p=0; p<=1; p++) {
final AxisDirection direction;
direction = AxisDirections.absolute(cs.getAxis(p==0 ? xDimension : yDimension).getDirection());
for (int i=0; i<DIRECTIONS.length; i++) {
if (direction.equals(DIRECTIONS[i])) {
final boolean needSwap = (i & 1) != p;
if (swap == null) {
swap = Boolean.valueOf(needSwap);
} else if (swap.booleanValue() != needSwap) {
swap = null; // Found an ambiguity; stop the search.
break control;
}
}
}
}
if (swap != null && swap.booleanValue()) {
this.xDimension = yDimension;
this.yDimension = xDimension;
} else {
this.xDimension = xDimension;
this.yDimension = yDimension;
}
}
assert temporalDimension>=0 && temporalDimension<dimension : temporalDimension;
coordinate = new GeneralDirectPosition(dimension); // Each instance must have its own.
}
/**
* Returns the coverage where the {@code evaluate} methods of this class delegate
* their work. By default this is the coverage specified at construction time.
*
* @return The coverage where sample values are evaluated.
*
* @since 2.2
*/
public Coverage getWrappedCoverage() {
return coverage;
}
/**
* The number of sample dimensions in the coverage.
* For grid coverages, a sample dimension is a band.
*
* @return The number of sample dimensions in the coverage.
*/
@Override
public int getNumSampleDimensions() {
return coverage.getNumSampleDimensions();
}
/**
* Retrieve sample dimension information for the coverage.
*
* @param index Index for sample dimension to retrieve. Indices are numbered 0 to
* (<var>{@linkplain #getNumSampleDimensions n}</var>-1).
* @return Sample dimension information for the coverage.
* @throws IndexOutOfBoundsException if {@code index} is out of bounds.
*/
@Override
public SampleDimension getSampleDimension(final int index) throws IndexOutOfBoundsException {
return coverage.getSampleDimension(index);
}
/**
* Returns the {@linkplain #getEnvelope envelope} geographic bounding box.
* The bounding box coordinates uses the {@linkplain DefaultGeographicCRS#WGS84 WGS84} CRS.
*
* @return The geographic bounding box.
* @throws TransformException if the envelope can't be transformed.
*/
public GeographicBoundingBox getGeographicBoundingBox() throws TransformException {
if (boundingBox == null) {
final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox();
bbox.setBounds(getEnvelope());
bbox.freeze();
boundingBox = bbox;
}
return boundingBox;
}
/**
* Returns the {@linkplain #getEnvelope envelope} time range.
*
* @return The time range of the envelope.
*/
public DateRange getTimeRange() {
final Envelope envelope = getEnvelope();
return new DateRange(temporalCRS.toDate(envelope.getMinimum(temporalDimension)),
temporalCRS.toDate(envelope.getMaximum(temporalDimension)));
}
/**
* Returns the dimension in the wrapped coverage for the specified dimension in this coverage.
* The {@code evaluate(Point2D, Date)} methods expect ordinates in the
* (<var>x</var>, <var>y</var>, <var>t</var>) order.
* The {@code evaluate(DirectPosition)} methods and the wrapped coverage way uses a different
* order.
*
* @param dimension A dimension in this coverage:
* 0 for <var>x</var>,
* 1 for <var>y</var> or
* 2 for <var>t</var>.
* @return The corresponding dimension in the wrapped coverage.
*
* @see #toDate
* @see #toPoint2D
* @see #toDirectPosition
*/
public final int toSourceDimension(final int dimension) {
switch (dimension) {
case 0: return xDimension;
case 1: return yDimension;
case 2: return temporalDimension;
default: throw new IllegalArgumentException();
}
}
/**
* Returns a coordinate point for the given spatial position and date.
*
* @param point The spatial position.
* @param date The date.
* @return The coordinate point.
*
* @see #toDate
* @see #toPoint2D
*
* @since 2.2
*/
public final DirectPosition toDirectPosition(final Point2D point, final Date date) {
coordinate.ordinates[ xDimension] = point.getX();
coordinate.ordinates[ yDimension] = point.getY();
coordinate.ordinates[temporalDimension] = temporalCRS.toValue(date);
return coordinate;
}
/**
* Returns the date for the specified direct position. This method (together with
* {@link #toPoint2D toPoint2D}) is the converse of {@link #toDirectPosition toDirectPosition}.
*
* @param position The direct position, as computed by
* {@link #toDirectPosition toDirectPosition}.
* @return The date.
*
* @see #toPoint2D
* @see #toDirectPosition
*
* @since 2.2
*/
public final Date toDate(final DirectPosition position) {
return temporalCRS.toDate(position.getOrdinate(temporalDimension));
}
/**
* Returns the spatial coordinate for the specified direct position. This method (together with
* {@link #toDate toDate}) is the converse of {@link #toDirectPosition toDirectPosition}.
*
* @param position The direct position, as computed by
* {@link #toDirectPosition toDirectPosition}.
* @return The spatial coordinate.
*
* @see #toDate
* @see #toDirectPosition
*
* @since 2.2
*/
public final Point2D toPoint2D(final DirectPosition position) {
return new Point2D.Double(position.getOrdinate(xDimension),
position.getOrdinate(yDimension));
}
/**
* Returns a sequence of boolean values for a given point in the coverage.
*
* @param point The coordinate point where to evaluate.
* @param time The date where to evaluate.
* @param dest An array in which to store values, or {@code null} to create a new array.
* @return The {@code dest} array, or a newly created array if {@code dest} was null.
* @throws PointOutsideCoverageException if {@code point} or {@code time} is outside coverage.
* @throws CannotEvaluateException if the computation failed for some other reason.
*/
public final boolean[] evaluate(final Point2D point, final Date time, boolean[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
try {
return evaluate(toDirectPosition(point, time), dest);
} catch (OrdinateOutsideCoverageException exception) {
if (exception.getOutOfBoundsDimension() == temporalDimension) {
exception = new OrdinateOutsideCoverageException(exception, time);
}
throw exception;
}
}
/**
* Returns a sequence of byte values for a given point in the coverage.
*
* @param point The coordinate point where to evaluate.
* @param time The date where to evaluate.
* @param dest An array in which to store values, or {@code null} to create a new array.
* @return The {@code dest} array, or a newly created array if {@code dest} was null.
* @throws PointOutsideCoverageException if {@code point} or {@code time} is outside coverage.
* @throws CannotEvaluateException if the computation failed for some other reason.
*/
public final byte[] evaluate(final Point2D point, final Date time, byte[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
try {
return evaluate(toDirectPosition(point, time), dest);
} catch (OrdinateOutsideCoverageException exception) {
if (exception.getOutOfBoundsDimension() == temporalDimension) {
exception = new OrdinateOutsideCoverageException(exception, time);
}
throw exception;
}
}
/**
* Returns a sequence of integer values for a given point in the coverage.
*
* @param point The coordinate point where to evaluate.
* @param time The date where to evaluate.
* @param dest An array in which to store values, or {@code null} to create a new array.
* @return The {@code dest} array, or a newly created array if {@code dest} was null.
* @throws PointOutsideCoverageException if {@code point} or {@code time} is outside coverage.
* @throws CannotEvaluateException if the computation failed for some other reason.
*/
public final int[] evaluate(final Point2D point, final Date time, int[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
try {
return evaluate(toDirectPosition(point, time), dest);
} catch (OrdinateOutsideCoverageException exception) {
if (exception.getOutOfBoundsDimension() == temporalDimension) {
exception = new OrdinateOutsideCoverageException(exception, time);
}
throw exception;
}
}
/**
* Returns a sequence of float values for a given point in the coverage.
*
* @param point The coordinate point where to evaluate.
* @param time The date where to evaluate.
* @param dest An array in which to store values, or {@code null} to create a new array.
* @return The {@code dest} array, or a newly created array if {@code dest} was null.
* @throws PointOutsideCoverageException if {@code point} or {@code time} is outside coverage.
* @throws CannotEvaluateException if the computation failed for some other reason.
*/
public final float[] evaluate(final Point2D point, final Date time, float[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
try {
return evaluate(toDirectPosition(point, time), dest);
} catch (OrdinateOutsideCoverageException exception) {
if (exception.getOutOfBoundsDimension() == temporalDimension) {
exception = new OrdinateOutsideCoverageException(exception, time);
}
throw exception;
}
}
/**
* Returns a sequence of double values for a given point in the coverage.
*
* @param point The coordinate point where to evaluate.
* @param time The date where to evaluate.
* @param dest An array in which to store values, or {@code null} to create a new array.
* @return The {@code dest} array, or a newly created array if {@code dest} was null.
* @throws PointOutsideCoverageException if {@code point} or {@code time} is outside coverage.
* @throws CannotEvaluateException if the computation failed for some other reason.
*/
public final double[] evaluate(final Point2D point, final Date time, double[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
try {
return evaluate(toDirectPosition(point, time), dest);
} catch (OrdinateOutsideCoverageException exception) {
if (exception.getOutOfBoundsDimension() == temporalDimension) {
exception = new OrdinateOutsideCoverageException(exception, time);
}
throw exception;
}
}
/**
* Returns the value vector for a given point in the coverage.
*
* @param coord The coordinate point where to evaluate.
* @throws PointOutsideCoverageException if {@code coord} is outside coverage.
* @throws CannotEvaluateException if the computation failed for some other reason.
*/
@Override
public final Object evaluate(final DirectPosition coord)
throws PointOutsideCoverageException, CannotEvaluateException
{
return coverage.evaluate(coord);
}
/**
* Returns a sequence of boolean values for a given point in the coverage.
*/
@Override
public final boolean[] evaluate(final DirectPosition coord, boolean[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
return coverage.evaluate(coord, dest);
}
/**
* Returns a sequence of byte values for a given point in the coverage.
*/
@Override
public final byte[] evaluate(final DirectPosition coord, byte[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
return coverage.evaluate(coord, dest);
}
/**
* Returns a sequence of integer values for a given point in the coverage.
*/
@Override
public final int[] evaluate(final DirectPosition coord, int[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
return coverage.evaluate(coord, dest);
}
/**
* Returns a sequence of float values for a given point in the coverage.
*/
@Override
public final float[] evaluate(final DirectPosition coord, float[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
return coverage.evaluate(coord, dest);
}
/**
* Returns a sequence of double values for a given point in the coverage.
*/
@Override
public final double[] evaluate(final DirectPosition coord, final double[] dest)
throws PointOutsideCoverageException, CannotEvaluateException
{
return coverage.evaluate(coord, dest);
}
/**
* Returns a 2 dimensional grid coverage for the given date. The grid geometry will be computed
* in order to produce image with the {@linkplain #getDefaultPixelSize() default pixel size},
* if any.
*
* @param time The date where to evaluate.
* @return The grid coverage at the specified time, or {@code null}
* if the requested date fall in a hole in the data.
* @throws PointOutsideCoverageException if {@code time} is outside coverage.
* @throws CannotEvaluateException if the computation failed for some other reason.
*
* @see #getRenderableImage(Date)
* @see RenderableImage#createDefaultRendering()
*/
public GridCoverage2D getGridCoverage2D(final Date time) throws CannotEvaluateException {
final InternationalString name = getName();
final CoordinateReferenceSystem crs = CRS.getHorizontalComponent(this.crs);
if (crs == null) {
throw new CannotEvaluateException(
Errors.format(Errors.Keys.CantSeparateCrs_1, this.crs.getName()));
}
final RenderedImage image = getRenderableImage(time).createDefaultRendering();
final GridSampleDimension[] bands = new GridSampleDimension[getNumSampleDimensions()];
for (int i=0; i<getNumSampleDimensions(); i++){
bands[i] = GridSampleDimension.castOrCopy(getSampleDimension(i));
}
final MathTransform gridToCRS;
gridToCRS = MathTransforms.linear((AffineTransform) image.getProperty("gridToCRS"));
if (factory == null) {
factory = CoverageFactoryFinder.getGridCoverageFactory(HINTS);
}
return factory.create(name, image, crs, gridToCRS, bands, null, null);
}
/**
* Returns 2D view of this grid coverage at the given date. For images produced by the
* {@linkplain RenderableImage#createDefaultRendering() default rendering}, the size
* will be computed from the {@linkplain #getDefaultPixelSize() default pixel size},
* if any.
*
* @param date The date where to evaluate the images.
* @return The renderable image.
*/
public RenderableImage getRenderableImage(final Date date) {
return new Renderable(date);
}
/**
* Constructs rendered images on demand.
*
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @since 2.1
* @module
*/
private final class Renderable extends AbstractCoverage.Renderable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 2541951932891099034L;
/**
* Construct a {@code Renderable} object for the supplied date.
*/
public Renderable(final Date date) {
super(xDimension, yDimension);
coordinate.ordinates[temporalDimension] = temporalCRS.toValue(date);
}
/**
* Returns a rendered image with width and height computed from
* {@link Coverage3D#getDefaultPixelSize()}.
*/
@Override
public RenderedImage createDefaultRendering() {
final Dimension2D pixelSize = getDefaultPixelSize();
if (pixelSize == null) {
return super.createDefaultRendering();
}
return createScaledRendering((int) Math.round(getWidth() / pixelSize.getWidth()),
(int) Math.round(getHeight() / pixelSize.getHeight()), null);
}
}
/**
* Returns the default pixel size for images to be produced by {@link #getRenderableImage(Date)}.
* This method is invoked by {@link RenderableImage#createDefaultRendering()} for computing a
* default image size. The default implementation for this method always returns {@code null}.
* Subclasses should overrides this method in order to provides a pixel size better suited to
* their data.
*
* @return The default pixel size, or {@code null} if no default is provided.
*/
protected Dimension2D getDefaultPixelSize() {
return null;
}
}