/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2003-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; import java.awt.geom.Point2D; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import java.awt.geom.AffineTransform; import java.awt.image.RenderedImage; import java.awt.image.renderable.RenderableImage; import java.util.Date; import javax.media.jai.util.Range; import org.opengis.coverage.Coverage; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.CannotEvaluateException; import org.opengis.referencing.FactoryException; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.CoordinateOperationFactory; 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.geotools.factory.Hints; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.geometry.GeneralDirectPosition; import org.geotools.referencing.CRS; import org.geotools.referencing.ReferencingFactoryFinder; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.referencing.crs.DefaultTemporalCRS; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.referencing.operation.TransformPathNotFoundException; import org.geotools.metadata.iso.extent.GeographicBoundingBoxImpl; import org.geotools.resources.geometry.XRectangle2D; import org.geotools.resources.CRSUtilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * 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 detects by itself which dimension is the time axis. It will also tries to uses * the {@code Point2D}'s {@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. * <br><br> * <strong>Note:</strong> This class is not thread safe for performance reasons. If desired, * users should create one instance of {@code SpatioTemporalCoverage3D} for each thread. * * @since 2.1 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class SpatioTemporalCoverage3D extends AbstractCoverage { /** * The hints for the creation of coordinate operation. * The default coordinate operation factory should be suffisient. */ 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 orientated 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_UP, AxisDirection.COLUMN_POSITIVE, AxisDirection.ROW_POSITIVE }; /** * The wrapped coverage. */ private final Coverage coverage; /** * The temporal coordinate system, as a Geotools implementation in order to gets the * {@link DefaultTemporalCRS#toDate} and {@link DefaultTemporalCRS#toValue} methods. */ private final DefaultTemporalCRS temporalCRS; /** * The dimension of the temporal coordinate system. * All other dimensions are expected to be the temporal ones. */ private final int temporalDimension; /** * The dimension for <var>x</var> and <var>y</var> coordinates. */ private 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( ErrorKeys.MISMATCHED_DIMENSION_$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.wrap(CRS.getTemporalCRS(crs)); if (temporalCRS == null) { throw new IllegalArgumentException( Errors.format(ErrorKeys.ILLEGAL_COORDINATE_REFERENCE_SYSTEM)); } 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 = cs.getAxis(p==0 ? xDimension : yDimension).getDirection().absolute(); 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 specified at construction time. * * @since 2.2 */ public final 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. */ 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. */ 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 Envelope envelope = getEnvelope(); Rectangle2D geographicArea = XRectangle2D.createFromExtremums( envelope.getMinimum(xDimension), envelope.getMinimum(yDimension), envelope.getMaximum(xDimension), envelope.getMaximum(yDimension)); final CoordinateReferenceSystem sourceCRS = CRS.getHorizontalCRS(crs); if (sourceCRS == null) { throw new TransformException(Errors.format(ErrorKeys.CANT_SEPARATE_CRS_$1, crs.getName())); } final CoordinateReferenceSystem targetCRS = DefaultGeographicCRS.WGS84; if (!CRS.equalsIgnoreMetadata(targetCRS, sourceCRS)) { final CoordinateOperation transform; final CoordinateOperationFactory factory; factory = ReferencingFactoryFinder.getCoordinateOperationFactory(HINTS); try { transform = factory.createOperation(sourceCRS, targetCRS); } catch (FactoryException exception) { throw new TransformPathNotFoundException(exception); } geographicArea = CRS.transform(transform, geographicArea, geographicArea); } boundingBox = (GeographicBoundingBox) new GeographicBoundingBoxImpl(geographicArea).unmodifiable(); } return boundingBox; } /** * Returns the {@linkplain #getEnvelope envelope} time range. * The returned range contains {@link Date} objects. */ public Range getTimeRange() { final Envelope envelope = getEnvelope(); return new Range(Date.class, 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 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 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 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 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 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. */ public final Object evaluate(final DirectPosition coord) throws 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 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 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 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 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 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 produces 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.getHorizontalCRS(this.crs); if (crs == null) { throw new CannotEvaluateException( Errors.format(ErrorKeys.CANT_SEPARATE_CRS_$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.wrap(getSampleDimension(i)); } final MathTransform gridToCRS; gridToCRS = ProjectiveTransform.create((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. * * @version $Id$ * @author Martin Desruisseaux (IRD) */ private final class Renderable extends AbstractCoverage.Renderable { /** * 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; } }