/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-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.referencing.operation.builder;
import java.util.Arrays;
import java.util.Objects;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.apache.sis.geometry.Envelope2D;
import org.geotoolkit.coverage.grid.GridEnvelope2D;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.internal.metadata.AxisDirections;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* A helper class for building <var>n</var>-dimensional {@linkplain AffineTransform affine transform}
* mapping {@linkplain GridEnvelope grid envelopes} to georeferenced {@linkplain Envelope envelopes}.
* The affine transform will be computed automatically from the information specified by the
* {@link #setGridExtent(GridEnvelope)} and {@link #setEnvelope(Envelope)} methods, which are
* mandatory. All other setter methods are optional hints about the affine transform to be created.
* <p>
* This builder is convenient when the following conditions are meet:
*
* <ul>
* <li><p>Pixels coordinates (usually (<var>x</var>,<var>y</var>) integer values inside
* the rectangle specified by the grid extent) 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}),
* <i>etc</i>.</p></li>
* </ul>
*
* In such case (and assuming that the image CRS has the same characteristics than the
* {@link BufferedImage} CRS described above):
*
* <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
* unnatural, but it reflects the fact that row values are increasing down in an
* {@link BufferedImage} CRS.</p></li>
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.20
*
* @since 2.3
* @module
*/
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 combination of bit masks telling which property were user-defined.
*
* @see #isAutomatic
* @see #setAutomatic
*/
private int defined;
/**
* The grid envelope, or {@code null} if not yet specified.
*/
private GridEnvelope gridExtent;
/**
* The geodetic 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 envelope and georeferenced envelope.
*
* @param gridExtent The extent of grid coordinates in a grid coverage.
* @param envelope The corresponding domain in user coordinate. This envelope must
* contains entirely all pixels, i.e. the envelope upper left corner must coincide
* with the upper left corner of the first pixel and the envelope lower right corner
* must coincide with the lower right corner of the last pixel.
* @throws MismatchedDimensionException if the two envelopes don't have consistent dimensions.
*/
public GridToEnvelopeMapper(final GridEnvelope gridExtent, final Envelope envelope)
throws MismatchedDimensionException
{
ensureNonNull("gridExtent", gridExtent);
ensureNonNull("envelope", envelope);
final int gridDim = gridExtent.getDimension();
final int userDim = envelope.getDimension();
if (userDim != gridDim) {
throw new MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_2, gridDim, userDim));
}
this.gridExtent = gridExtent;
this.envelope = envelope;
}
/**
* Makes sure that the specified objects have the same dimension.
*/
private static void ensureDimensionMatch(final GridEnvelope gridExtent,
final Envelope envelope, final boolean checkingRange)
{
if (gridExtent != null && envelope != null) {
final String label;
final int dim1, dim2;
if (checkingRange) {
label = "gridExtent";
dim1 = gridExtent.getDimension();
dim2 = envelope .getDimension();
} else {
label = "envelope";
dim1 = envelope .getDimension();
dim2 = gridExtent.getDimension();
}
if (dim1 != dim2) {
throw new MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_3, label, dim1, dim2));
}
}
}
/**
* Flushes 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 coordinates map {@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 coordinates map pixel center or corner.
*
* @since 2.5
*/
public PixelInCell getPixelAnchor() {
return anchor;
}
/**
* Sets whatever the grid coordinates map {@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).
*
* @param anchor Whatever the grid coordinates map pixel center or corner.
*
* @since 2.5
*/
public void setPixelAnchor(final PixelInCell anchor) {
ensureNonNull("anchor", anchor);
if (!Objects.equals(this.anchor, anchor)) {
this.anchor = anchor;
reset();
}
}
/**
* Returns the The extent of grid coordinates in a grid coverage.
*
* @return The The extent of grid coordinates in a grid coverage.
* @throws IllegalStateException if the grid envelope has not yet been defined.
*/
public GridEnvelope getGridExtent() throws IllegalStateException {
if (gridExtent == null) {
throw new IllegalStateException(Errors.format(
Errors.Keys.NoParameterValue_1, "gridEnvelope"));
}
return gridExtent;
}
/**
* Sets the The extent of grid coordinates in a grid coverage.
*
* @param extent The new grid envelope.
*
* @since 3.20 (derived from 2.3)
*/
public void setGridExtent(final GridEnvelope extent) {
ensureNonNull("extent", extent);
ensureDimensionMatch(extent, envelope, true);
if (!Objects.equals(gridExtent, extent)) {
gridExtent = extent;
reset();
}
}
/**
* Sets the grid envelope as a two-dimensional rectangle. This convenience method
* creates a {@link GridEnvelope2D} from the given rectangle and delegates to the
* {@link #setGridExtent(GridEnvelope)} method.
*
* @param extent The new grid envelope.
*
* @since 3.20 (derived from 3.15)
*/
public void setGridExtent(final Rectangle extent) {
final GridEnvelope ge;
if (extent instanceof GridEnvelope) {
ge = (GridEnvelope) extent;
} else {
ensureNonNull("gridEnvelope", extent);
ge = new GridEnvelope2D(extent);
}
setGridExtent(ge);
}
/**
* Sets the grid envelope as a two-dimensional rectangle. This convenience method
* creates a {@link GridEnvelope2D} from the given rectangle and delegates to the
* {@link #setGridExtent(GridEnvelope)} method.
*
* @param x The minimal <var>x</var> ordinate.
* @param y The minimal <var>y</var> ordinate.
* @param width The number of valid ordinates along the <var>x</var> axis.
* @param height The number of valid ordinates along the <var>y</var> axis.
*
* @since 3.20 (derived from 3.15)
*/
public void setGridExtent(final int x, final int y, final int width, final int height) {
setGridExtent((GridEnvelope) new GridEnvelope2D(x, y, width, height));
}
/**
* Returns the georeferenced envelope. For performance reason, this method does 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(
Errors.Keys.NoParameterValue_1, "envelope"));
}
return envelope;
}
/**
* Sets the georeferenced 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(gridExtent, envelope, false);
if (!Objects.equals(this.envelope, envelope)) {
this.envelope = envelope;
reset();
}
}
/**
* Sets the envelope as a two-dimensional rectangle. This convenience method creates an
* {@link Envelope2D} from the given rectangle and delegates to the
* {@link #setEnvelope(Envelope)} method.
*
* @param envelope The new envelope.
*
* @since 3.15
*/
public void setEnvelope(final Rectangle2D envelope) {
final Envelope env;
if (envelope instanceof Envelope) {
env = (Envelope) envelope;
} else {
ensureNonNull("envelope", envelope);
env = new Envelope2D(null, envelope);
}
setEnvelope(env);
}
/**
* Sets the envelope as a two-dimensional rectangle. This convenience method creates an
* {@link Envelope2D} from the given rectangle and delegates to the
* {@link #setEnvelope(Envelope)} method.
*
* @param x The <var>x</var> minimal value.
* @param y The <var>y</var> minimal value.
* @param width The envelope width.
* @param height The envelope height.
*
* @since 3.15
*/
public void setEnvelope(final double x, final double y, final double width, final double height) {
setEnvelope((Envelope) new Envelope2D(null, x, y, width, height));
}
/**
* 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(AxisDirections.absolute(cs.getAxis(0).getDirection())) &&
AxisDirection.EAST .equals(AxisDirections.absolute(cs.getAxis(1).getDirection()));
}
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 envelope matches exactly axis order in the genreferenced 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 makes the following assumptions:
* <p>
* <ul>
* <li>Axis should be reverted if needed in order to have the most commonly used
* direction for increasing positive values (North, East, Up, Future).</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++) {
reverseAxis[i] = AxisDirections.isOpposite(cs.getAxis(i).getDirection());
}
if (dimension >= 2) {
final int i = getSwapXY() ? 0 : 1;
reverseAxis[i] = !reverseAxis[i];
}
}
} else {
// No coordinate system. Reverse the second axis unconditionally
// (except if there is not enough dimensions).
int length = 0;
if (gridExtent != null) {
length = gridExtent.getDimension();
} else if (envelope != null) {
length = envelope.getDimension();
}
if (length >= 2) {
reverseAxis = new boolean[length];
reverseAxis[1] = true;
}
}
}
return (reverseAxis != null) ? reverseAxis.clone() : null;
}
/**
* Sets 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(boolean[] reverse) {
if (reverse != null) {
reverse = reverse.clone();
}
if (!Arrays.equals(reverseAxis, reverse)) {
reset();
}
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 (gridExtent != null) {
length = gridExtent.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 combination 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;
}
/**
* Sets 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 combination 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 = envelope.getCoordinateReferenceSystem();
if (crs != null) {
return crs.getCoordinateSystem();
}
}
return null;
}
/**
* Creates a <cite>Grid to Envelope</cite> (or <cite>grid to CRS</cite>) transform using
* the information provided by setter methods. The default implementation returns an instance
* of {@link LinearTransform}, but subclasses could create more complex transforms.
*
* @return The <cite>grid to CRS</cite> transform.
* @throws IllegalStateException if the grid envelope or the georeferenced envelope were not set.
*/
public MathTransform createTransform() throws IllegalStateException {
if (transform == null) {
final GridEnvelope gridEnvelope = getGridExtent();
final Envelope userEnvelope = getEnvelope();
final boolean swapXY = getSwapXY();
final boolean[] reverse = getReverseAxis();
final PixelInCell gridType = getPixelAnchor();
final int dimension = gridEnvelope.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(
Errors.Keys.IllegalArgument_2, "gridType", gridType));
}
final Matrix matrix = Matrices.createIdentity(dimension + 1);
for (int i=0; i<dimension; i++) {
// NOTE: i is a dimension in the 'gridEnvelope' space (source coordinates).
// j is a dimension in the 'userEnvelope' space (target coordinates).
int j = i;
if (swapXY && j<=1) {
j = 1-j;
}
double scale = userEnvelope.getSpan(j) / gridEnvelope.getSpan(i);
double offset;
if (reverse == null || j >= reverse.length || !reverse[j]) {
offset = userEnvelope.getMinimum(j);
} else {
scale = -scale;
offset = userEnvelope.getMaximum(j);
}
offset -= scale * (gridEnvelope.getLow(i) - translate);
matrix.setElement(j, j, 0.0 );
matrix.setElement(j, i, scale );
matrix.setElement(j, dimension, offset);
}
transform = MathTransforms.linear(matrix);
}
return transform;
}
/**
* Returns the <cite>Grid to Envelope</cite> (or <cite>grid to CRS</cite>)
* transform as a two-dimensional affine transform.
*
* @return The <cite>grid to CRS</cite> 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(Errors.Keys.NotAnAffineTransform));
}
}