/*
* 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.referencing.operation.builder;
import java.util.Arrays;
import java.awt.image.BufferedImage;
import java.awt.geom.AffineTransform;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.Matrix;
import org.geotools.referencing.operation.matrix.MatrixFactory;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.util.Utilities;
/**
* A helper class for building <var>n</var>-dimensional {@linkplain AffineTransform
* affine transform} mapping {@linkplain GridEnvelope grid ranges} to {@linkplain Envelope
* envelopes}. The affine transform will be computed automatically from the information
* specified by the {@link #setGridRange setGridRange} and {@link #setEnvelope setEnvelope}
* methods, which are mandatory. All other setter methods are optional hints about the
* affine transform to be created. This builder is convenient when the following conditions
* are meet:
* <p>
* <ul>
* <li><p>Pixels coordinates (usually (<var>x</var>,<var>y</var>) integer values inside
* the rectangle specified by the grid range) are expressed in some
* {@linkplain CoordinateReferenceSystem coordinate reference system} known at compile
* time. This is often the case. For example the CRS attached to {@link BufferedImage}
* has always ({@linkplain AxisDirection#COLUMN_POSITIVE column},
* {@linkplain AxisDirection#ROW_POSITIVE row}) axis, with the origin (0,0) in the upper
* left corner, and row values increasing down.</p></li>
*
* <li><p>"Real world" coordinates (inside the envelope) are expressed in arbitrary
* <em>horizontal</em> coordinate reference system. Axis directions may be
* ({@linkplain AxisDirection#NORTH North}, {@linkplain AxisDirection#WEST West}),
* or ({@linkplain AxisDirection#EAST East}, {@linkplain AxisDirection#NORTH North}),
* <cite>etc.</cite>.</p></li>
* </ul>
* <p>
* In such case (and assuming that the image's CRS has the same characteristics than the
* {@link BufferedImage}'s CRS described above):
* <p>
* <ul>
* <li><p>{@link #setSwapXY swapXY} shall be set to {@code true} if the "real world" axis
* order is ({@linkplain AxisDirection#NORTH North}, {@linkplain AxisDirection#EAST East})
* instead of ({@linkplain AxisDirection#EAST East}, {@linkplain AxisDirection#NORTH North}).
* This axis swapping is necessary for mapping the ({@linkplain AxisDirection#COLUMN_POSITIVE
* column}, {@linkplain AxisDirection#ROW_POSITIVE row}) axis order associated to the
* image CRS.</p></li>
*
* <li><p>In addition, the "real world" axis directions shall be reversed (by invoking
* <code>{@linkplain #reverseAxis reverseAxis}(dimension)</code>) if their direction is
* {@link AxisDirection#WEST WEST} (<var>x</var> axis) or {@link AxisDirection#NORTH NORTH}
* (<var>y</var> axis), in order to get them oriented toward the {@link AxisDirection#EAST
* EAST} or {@link AxisDirection#SOUTH SOUTH} direction respectively. The later may seems
* unatural, but it reflects the fact that row values are increasing down in an
* {@link BufferedImage}'s CRS.</p></li>
* </ul>
*
* @since 2.3
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class GridToEnvelopeMapper {
/**
* A bit mask for the {@link #setSwapXY swapXY} property.
*
* @see #isAutomatic
* @see #setAutomatic
*/
public static final int SWAP_XY = 1;
/**
* A bit mask for the {@link #setReverseAxis reverseAxis} property.
*
* @see #isAutomatic
* @see #setAutomatic
*/
public static final int REVERSE_AXIS = 2;
/**
* A combinaison of bit masks telling which property were user-defined.
*
* @see #isAutomatic
* @see #setAutomatic
*/
private int defined;
/**
* The grid range, or {@code null} if not yet specified.
*/
private GridEnvelope gridRange;
/**
* The envelope, or {@code null} if not yet specified.
*/
private Envelope envelope;
/**
* Whatever the {@code gridToCRS} transform will maps pixel center or corner.
* The default value is {@link PixelInCell#CELL_CENTER}.
*/
private PixelInCell anchor = PixelInCell.CELL_CENTER;
/**
* {@code true} if we should swap the two first axis, {@code false} if we should
* not swap and {@code null} if this state is not yet determined.
*/
private Boolean swapXY;
/**
* The axis to reverse, or {@code null} if none or not yet determined.
*/
private boolean[] reverseAxis;
/**
* The math transform, or {@code null} if not yet computed.
*/
private MathTransform transform;
/**
* Creates a new instance of {@code GridToEnvelopeMapper}.
*/
public GridToEnvelopeMapper() {
}
/**
* Creates a new instance for the specified grid range and envelope.
*
* @param gridRange The valid coordinate range of a grid coverage.
* @param userRange The corresponding coordinate range in user coordinate. This envelope must
* contains entirely all pixels, i.e. the envelope's upper left corner must
* coincide with the upper left corner of the first pixel and the envelope's
* lower right corner must coincide with the lower right corner of the last
* pixel.
*
* @throws MismatchedDimensionException if the grid range and the envelope doesn't have
* consistent dimensions.
*/
public GridToEnvelopeMapper(final GridEnvelope gridRange, final Envelope userRange)
throws MismatchedDimensionException
{
ensureNonNull("gridRange", gridRange);
ensureNonNull("userRange", userRange);
final int gridDim = gridRange.getDimension();
final int userDim = userRange.getDimension();
if (userDim != gridDim) {
throw new MismatchedDimensionException(Errors.format(ErrorKeys.MISMATCHED_DIMENSION_$2,
gridDim, userDim));
}
this.gridRange = gridRange;
this.envelope = userRange;
}
/**
* Makes sure that an argument is non-null.
*/
private static void ensureNonNull(final String name, final Object object)
throws IllegalArgumentException
{
if (object == null) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name));
}
}
/**
* Makes sure that the specified objects have the same dimension.
*/
private static void ensureDimensionMatch(final GridEnvelope gridRange,
final Envelope envelope,
final boolean checkingRange)
{
if (gridRange != null && envelope != null) {
final String label;
final int dim1, dim2;
if (checkingRange) {
label = "gridRange";
dim1 = gridRange.getDimension();
dim2 = envelope .getDimension();
} else {
label = "envelope";
dim1 = envelope .getDimension();
dim2 = gridRange.getDimension();
}
if (dim1 != dim2) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$3, label, dim1, dim2));
}
}
}
/**
* Flush any information cached in this object.
*/
private void reset() {
transform = null;
if (isAutomatic(REVERSE_AXIS)) {
reverseAxis = null;
}
if (isAutomatic(SWAP_XY)) {
swapXY = null;
}
}
/**
* Returns whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
* or {@linkplain PixelInCell#CELL_CORNER pixel corner}.
*
* @deprecated Renamed {@link #getPixelAnchor}.
*/
@Deprecated
public PixelInCell getGridType() {
return getPixelAnchor();
}
/**
* Returns whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
* or {@linkplain PixelInCell#CELL_CORNER pixel corner}. The former is OGC convention while
* the later is Java2D/JAI convention. The default is cell center (OGC convention).
*
* @return Whatever the grid range maps pixel center or corner.
*
* @since 2.5
*/
public PixelInCell getPixelAnchor() {
return anchor;
}
/**
* Set whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
* or {@linkplain PixelInCell#CELL_CORNER pixel corner}.
*
* @deprecated Renamed {@link #setPixelAnchor}.
*/
@Deprecated
public void setGridType(final PixelInCell anchor) {
setPixelAnchor(anchor);
}
/**
* Sets whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
* or {@linkplain PixelInCell#CELL_CORNER pixel corner}. The former is OGC convention
* while the later is Java2D/JAI convention.
*
* @param anchor Whatever the grid range maps pixel center or corner.
*
* @since 2.5
*/
public void setPixelAnchor(final PixelInCell anchor) {
ensureNonNull("anchor", anchor);
if (!Utilities.equals(this.anchor, anchor)) {
this.anchor = anchor;
reset();
}
}
/**
* Returns the grid range.
*
* @return The grid range.
* @throws IllegalStateException if the grid range has not yet been defined.
*/
public GridEnvelope getGridRange() throws IllegalStateException {
if (gridRange == null) {
throw new IllegalStateException(Errors.format(
ErrorKeys.MISSING_PARAMETER_VALUE_$1, "gridRange"));
}
return gridRange;
}
/**
* Sets the grid range.
*
* @param gridRange The new grid range.
*/
public void setGridRange(final GridEnvelope gridRange) {
ensureNonNull("gridRange", gridRange);
ensureDimensionMatch(gridRange, envelope, true);
if (!Utilities.equals(this.gridRange, gridRange)) {
this.gridRange = gridRange;
reset();
}
}
/**
* Returns the envelope. For performance reason, this method do not
* clone the envelope. So the returned object should not be modified.
*
* @return The envelope.
* @throws IllegalStateException if the envelope has not yet been defined.
*/
public Envelope getEnvelope() throws IllegalStateException {
if (envelope == null) {
throw new IllegalStateException(Errors.format(
ErrorKeys.MISSING_PARAMETER_VALUE_$1, "envelope"));
}
return envelope;
}
/**
* Sets the envelope. This method do not clone the specified envelope,
* so it should not be modified after this method has been invoked.
*
* @param envelope The new envelope.
*/
public void setEnvelope(final Envelope envelope) {
ensureNonNull("envelope", envelope);
ensureDimensionMatch(gridRange, envelope, false);
if (!Utilities.equals(this.envelope, envelope)) {
this.envelope = envelope;
reset();
}
}
/**
* Applies heuristic rules in order to determine if the two first axis should be interchanged.
*/
private static boolean swapXY(final CoordinateSystem cs) {
if (cs != null && cs.getDimension() >= 2) {
return AxisDirection.NORTH.equals(cs.getAxis(0).getDirection().absolute()) &&
AxisDirection.EAST .equals(cs.getAxis(1).getDirection().absolute());
}
return false;
}
/**
* Returns {@code true} if the two first axis should be interchanged. If
* <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #SWAP_XY})</code>
* returns {@code true} (which is the default), then this method make the
* following assumptions:
*
* <ul>
* <li><p>Axis order in the grid range matches exactly axis order in the envelope, except
* for the special case described in the next point. In other words, if axis order in
* the underlying image is (<var>column</var>, <var>row</var>) (which is the case for
* a majority of images), then the envelope should probably have a (<var>longitude</var>,
* <var>latitude</var>) or (<var>easting</var>, <var>northing</var>) axis order.</p></li>
*
* <li><p>An exception to the above rule applies for CRS using exactly the following axis
* order: ({@link AxisDirection#NORTH NORTH}|{@link AxisDirection#SOUTH SOUTH},
* {@link AxisDirection#EAST EAST}|{@link AxisDirection#WEST WEST}). An example
* of such CRS is {@code EPSG:4326}. In this particular case, this method will
* returns {@code true}, thus suggesting to interchange the
* (<var>y</var>,<var>x</var>) axis for such CRS.</p></li>
* </ul>
*
* @return {@code true} if the two first axis should be interchanged.
*/
public boolean getSwapXY() {
if (swapXY == null) {
boolean value = false;
if (isAutomatic(SWAP_XY)) {
value = swapXY(getCoordinateSystem());
}
swapXY = Boolean.valueOf(value);
}
return swapXY.booleanValue();
}
/**
* Tells if the two first axis should be interchanged. Invoking this method force
* <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #SWAP_XY})</code> to
* {@code false}.
*
* @param swapXY {@code true} if the two first axis should be interchanged.
*/
public void setSwapXY(final boolean swapXY) {
final Boolean newValue = Boolean.valueOf(swapXY);
if (!newValue.equals(this.swapXY)) {
reset();
}
this.swapXY = newValue;
defined |= SWAP_XY;
}
/**
* Returns which (if any) axis in <cite>user</cite> space
* (not grid space) should have their direction reversed. If
* <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #REVERSE_AXIS})</code>
* returns {@code true} (which is the default), then this method make the
* following assumptions:
* <p>
* <ul>
* <li>Axis should be reverted if needed in order to point toward their
* "{@linkplain AxisDirection#absolute absolute}" direction.</li>
* <li>An exception to the above rule is the second axis in grid space,
* which is assumed to be the <var>y</var> axis on output device (usually
* the screen). This axis is reversed again in order to match the bottom
* direction often used with such devices.</li>
* </ul>
*
* @return The reversal state of each axis, or {@code null} if unspecified.
*/
public boolean[] getReverseAxis() {
if (reverseAxis == null) {
final CoordinateSystem cs = getCoordinateSystem();
if (cs != null) {
final int dimension = cs.getDimension();
reverseAxis = new boolean[dimension];
if (isAutomatic(REVERSE_AXIS)) {
for (int i=0; i<dimension; i++) {
final AxisDirection direction = cs.getAxis(i).getDirection();
final AxisDirection absolute = direction.absolute();
reverseAxis[i] = direction.equals(absolute.opposite());
}
if (dimension >= 2) {
final int i = getSwapXY() ? 0 : 1;
reverseAxis[i] = !reverseAxis[i];
}
}
} else {
// No coordinate system. Reverse the second axis inconditionnaly
// (except if there is not enough dimensions).
int length = 0;
if (gridRange != null) {
length = gridRange.getDimension();
} else if (envelope != null) {
length = envelope.getDimension();
}
if (length >= 2) {
reverseAxis = new boolean[length];
reverseAxis[1] = true;
}
}
}
return reverseAxis;
}
/**
* Set which (if any) axis in <cite>user</cite> space (not grid space)
* should have their direction reversed. Invoking this method force
* <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #REVERSE_AXIS})</code>
* to {@code false}.
*
* @param reverse The reversal state of each axis. A {@code null} value means to
* reverse no axis.
*/
public void setReverseAxis(final boolean[] reverse) {
if (!Arrays.equals(reverseAxis, reverse)) {
reset();
}
this.reverseAxis = reverse;
defined |= REVERSE_AXIS;
}
/**
* Reverses a single axis in user space. Invoking this methods <var>n</var> time
* is equivalent to creating a boolean {@code reverse} array of the appropriate length,
* setting {@code reverse[dimension] = true} for the <var>n</var> axis to be reversed,
* and invoke <code>{@linkplain #setReverseAxis setReverseAxis}(reverse)</code>.
*
* @param dimension The index of the axis to reverse.
*/
public void reverseAxis(final int dimension) {
if (reverseAxis == null) {
final int length;
if (gridRange != null) {
length = gridRange.getDimension();
} else {
ensureNonNull("envelope", envelope);
length = envelope.getDimension();
}
reverseAxis = new boolean[length];
}
if (!reverseAxis[dimension]) {
reset();
}
reverseAxis[dimension] = true;
defined |= REVERSE_AXIS;
}
/**
* Returns {@code true} if all properties designed by the specified bit mask
* will be computed automatically.
*
* @param mask Any combinaison of {@link #REVERSE_AXIS} or {@link #SWAP_XY}.
* @return {@code true} if all properties given by the mask will be computed automatically.
*/
public boolean isAutomatic(final int mask) {
return (defined & mask) == 0;
}
/**
* Set all properties designed by the specified bit mask as automatic. Their
* value will be computed automatically by the corresponding methods (e.g.
* {@link #getReverseAxis}, {@link #getSwapXY}). By default, all properties
* are automatic.
*
* @param mask Any combinaison of {@link #REVERSE_AXIS} or {@link #SWAP_XY}.
*/
public void setAutomatic(final int mask) {
defined &= ~mask;
}
/**
* Returns the coordinate system in use with the envelope.
*/
private CoordinateSystem getCoordinateSystem() {
if (envelope != null) {
final CoordinateReferenceSystem crs;
crs = envelope.getCoordinateReferenceSystem();
if (crs != null) {
return crs.getCoordinateSystem();
}
}
return null;
}
/**
* Creates a math transform using the information provided by setter methods.
*
* @return The math transform.
* @throws IllegalStateException if the grid range or the envelope were not set.
*/
public MathTransform createTransform() throws IllegalStateException {
if (transform == null) {
final GridEnvelope gridRange = getGridRange();
final Envelope userRange = getEnvelope();
final boolean swapXY = getSwapXY();
final boolean[] reverse = getReverseAxis();
final PixelInCell gridType = getPixelAnchor();
final int dimension = gridRange.getDimension();
/*
* Setup the multi-dimensional affine transform for use with OpenGIS.
* According OpenGIS specification, transforms must map pixel center.
* This is done by adding 0.5 to grid coordinates.
*/
final double translate;
if (PixelInCell.CELL_CENTER.equals(gridType)) {
translate = 0.5;
} else if (PixelInCell.CELL_CORNER.equals(gridType)) {
translate = 0.0;
} else {
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"gridType", gridType));
}
final Matrix matrix = MatrixFactory.create(dimension + 1);
for (int i=0; i<dimension; i++) {
// NOTE: i is a dimension in the 'gridRange' space (source coordinates).
// j is a dimension in the 'userRange' space (target coordinates).
int j = i;
if (swapXY && j<=1) {
j = 1-j;
}
double scale = userRange.getSpan(j) / gridRange.getSpan(i);
double offset;
if (reverse==null || j>=reverse.length || !reverse[j]) {
offset = userRange.getMinimum(j);
} else {
scale = -scale;
offset = userRange.getMaximum(j);
}
offset -= scale * (gridRange.getLow(i) - translate);
matrix.setElement(j, j, 0.0 );
matrix.setElement(j, i, scale );
matrix.setElement(j, dimension, offset);
}
transform = ProjectiveTransform.create(matrix);
}
return transform;
}
/**
* Returns the math transform as a two-dimensional affine transform.
*
* @return The math transform as a two-dimensional affine transform.
* @throws IllegalStateException if the math transform is not of the appropriate type.
*/
public AffineTransform createAffineTransform() throws IllegalStateException {
final MathTransform transform = createTransform();
if (transform instanceof AffineTransform) {
return (AffineTransform) transform;
}
throw new IllegalStateException(Errors.format(ErrorKeys.NOT_AN_AFFINE_TRANSFORM));
}
}