/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-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.internal.image.io;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.geom.AffineTransform;
import javax.imageio.metadata.IIOMetadata;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.GridGeometry;
import org.opengis.metadata.spatial.CellGeometry;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.datum.PixelInCell;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.image.io.metadata.MetadataHelper;
import org.geotoolkit.image.io.metadata.MetadataNodeAccessor;
import org.geotoolkit.referencing.cs.DiscreteReferencingFactory;
import org.geotoolkit.metadata.iso.spatial.PixelTranslation;
import org.geotoolkit.referencing.operation.matrix.Matrices;
import org.geotoolkit.resources.Errors;
import static org.geotoolkit.image.io.MultidimensionalImageStore.*;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME;
import static org.geotoolkit.internal.image.io.DimensionAccessor.fixRoundingError;
/**
* A convenience specialization of {@link MetadataNodeAccessor} for nodes related to the
* {@code "RectifiedGridDomain"} node. This class provides also convenience methods
* for the {@code "RectifiedGridDomain/Limits"} and the {@code "SpatialRepresentation"}
* nodes.
* <p>
* For most usage, the following methods should be invoked exactly once.
* They will take care of invoking the appropriate setter methods.
* <p>
* <ul>
* <li>{@link #setSpatialRepresentation setSpatialRepresentation}</li>
* <li>{@link #setRectifiedGridDomain setRectifiedGridDomain}</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.19
*
* @since 3.06
* @module
*/
public final class GridDomainAccessor extends MetadataNodeAccessor {
/**
* The name of the single attribute to declare for node that contains array.
* This is used mostly for the following:
*
* {@preformat text
* RectifiedGridDomain : RectifiedGrid
* └───OffsetVectors : List<double[]>
* └───OffsetVector : double[]
* └───values
* }
*/
public static final String ARRAY_ATTRIBUTE_NAME = "values";
/**
* Threshold for floating point comparisons.
*/
private static final double EPS = 1E-10;
/**
* The accessor for offset vectors. Will be created only when first needed.
*/
private MetadataNodeAccessor offsetVectors;
/**
* Creates a new accessor for the given metadata.
*
* @param metadata The Image I/O metadata. An instance of the
* {@link org.geotoolkit.image.io.metadata.SpatialMetadata}
* sub-class is recommended, but not mandatory.
*/
public GridDomainAccessor(final IIOMetadata metadata) {
super(metadata, GEOTK_FORMAT_NAME, "RectifiedGridDomain", null);
}
/**
* Sets the limits, origin and offset vectors from the given grid geometry.
* The <cite>grid to CRS</cite> transform needs to be linear in order to get
* the offset vectors formatted.
* <p>
* The value of the {@code pixelInCell} argument has an impact on the value of the
* {@code origin} attribute. If null, the {@link PixelInCell.CELL_CENTER} value is
* normally assumed, but the behavior could be different if the user overridden the
* {@link GridGeometry#getGridToCRS()} method. See
* {@link DiscreteReferencingFactory#getAffineTransform(GridGeometry, PixelInCell)}
* for more information.
*
* @param geometry The grid geometry.
* @param pixelInCell The value to assign to the {@code "pointInPixel"} attribute, or {@code null}.
* @param cellGeometry The value to assign to the {@code "cellGeometry"} attribute, or {@code null}.
*/
public void setGridGeometry(final GridGeometry geometry, final PixelInCell pixelInCell, final CellGeometry cellGeometry) {
setGridGeometry(geometry, pixelInCell, cellGeometry, -1);
}
/**
* @deprecated The {@code axisToReverse} argument needs to be removed.
*
* @param axisToReverse The axis to reverse (typically 1 for the <var>y</var> axis), or -1 if none.
*
* @since 3.15
*/
@Deprecated
public void setGridGeometry(final GridGeometry geometry, final PixelInCell pixelInCell,
final CellGeometry cellGeometry, final int axisToReverse)
{
final GridEnvelope gridEnvelope = geometry.getExtent();
if (gridEnvelope != null) {
final int gridDimension = gridEnvelope.getDimension();
final int[] lower = new int[gridDimension];
final int[] upper = new int[gridDimension];
for (int i=0; i<gridDimension; i++) {
lower[i] = gridEnvelope.getLow (i);
upper[i] = gridEnvelope.getHigh(i);
}
final MathTransform gridToCRS = geometry.getGridToCRS(); // Really want pixel center.
if (gridToCRS != null) {
final int crsDimension = gridToCRS.getTargetDimensions();
double[] center = new double[Math.max(crsDimension, gridDimension)];
for (int i=0; i<gridDimension; i++) {
center[i] = 0.5 * (lower[i] + upper[i]);
}
try {
gridToCRS.transform(center, 0, center, 0, 1);
center = fixRoundingError(ArraysExt.resize(center, crsDimension));
setSpatialRepresentation(center, cellGeometry, PixelTranslation.getPixelOrientation(pixelInCell));
} catch (TransformException e) {
// Should not happen. If it happen anyway, this is not a fatal error.
// The above metadata will be missing from the IIOMetadata object, but
// they were mostly for information purpose anyway.
Logging.unexpectedException(null, GridDomainAccessor.class, "setGridGeometry", e);
}
}
setLimits(lower, upper);
}
/*
* Now set the origin and offset vectors, which are inferred from the gridToCRS matrix.
* This is possible only if the transform is affine.
*/
final Matrix matrix = DiscreteReferencingFactory.getAffineTransform(geometry, pixelInCell);
if (matrix != null) {
if (axisToReverse >= 0) {
if (gridEnvelope == null) {
return; // Can't write a correct origin without this information.
}
int span = gridEnvelope.getSpan(axisToReverse);
if (pixelInCell == null || pixelInCell.equals(PixelInCell.CELL_CENTER)) {
span--;
}
Matrices.reverseAxisDirection(matrix, axisToReverse, span);
}
final int gridDimension = matrix.getNumCol() - 1;
final int crsDimension = matrix.getNumRow() - 1;
final double[] origin = new double[crsDimension];
for (int j=0; j<crsDimension; j++) {
origin[j] = matrix.getElement(j, gridDimension);
}
setOrigin(fixRoundingError(origin));
clearOffsetVectors();
final double[] vector = new double[gridDimension];
for (int j=0; j<crsDimension; j++) {
for (int i=0; i<gridDimension; i++) {
vector[i] = matrix.getElement(j, i);
}
addOffsetVector(fixRoundingError(vector));
}
}
}
/**
* Sets the {@code "low"} and {@code "high"} attributes of the
* {@code "RectifiedGridDomain/Limits"} node to the given values.
*
* @param low The value to be assigned to the {@code "low"} attribute.
* @param high The value to be assigned to the {@code "high"} attribute.
*/
public void setLimits(final int[] low, final int[] high) {
final MetadataNodeAccessor accessor = new MetadataNodeAccessor(this, "Limits", null);
accessor.setAttribute("low", low);
accessor.setAttribute("high", high);
}
/**
* Gets the {@code "low"} and {@code "high"} attributes of the
* {@code "RectifiedGridDomain/Limits"} node.
*
* @return int[][] :
* int[0] is the lower bounds,
* int[1] is the upper bounds
*/
public int[][] getLimits() {
final MetadataNodeAccessor accessor = new MetadataNodeAccessor(this, "Limits", null);
return new int[][]{
accessor.getAttributeAsIntegers("low", false),
accessor.getAttributeAsIntegers("high", false)
};
}
/**
* Sets the origin and offset vectors from the given affine transform.
*
* @param gridToCRS The affine transform to use for setting the origin and offset vectors.
*
* @since 3.19
*/
public void setGridToCRS(final AffineTransform gridToCRS) {
final double[] vector = new double[] {
gridToCRS.getTranslateX(), // X_DIMENSION
gridToCRS.getTranslateY() // Y_DIMENSION
};
setOrigin(fixRoundingError(vector));
clearOffsetVectors();
vector[X_DIMENSION]=gridToCRS.getScaleX(); vector[Y_DIMENSION]=gridToCRS.getShearY(); addOffsetVector(fixRoundingError(vector));
vector[X_DIMENSION]=gridToCRS.getShearX(); vector[Y_DIMENSION]=gridToCRS.getScaleY(); addOffsetVector(fixRoundingError(vector));
}
/**
* Sets the {@code "origin"} attribute to the given value.
*
* @param values The value to be assigned to the {@code "origin"} attribute.
*/
public void setOrigin(final double... values) {
setAttribute("origin", values);
}
/**
* Appends a new {@code "OffsetVector"} node with the {@code "values"}
* attribute set to the given array.
*
* @param values The value to be assigned to the {@code "values"} attribute
* of a new {@code "OffsetVector"} node.
*/
public void addOffsetVector(final double... values) {
MetadataNodeAccessor accessor = offsetVectors;
if (accessor == null) {
offsetVectors = accessor = new MetadataNodeAccessor(this, "OffsetVectors", "OffsetVector");
}
accessor.selectChild(accessor.appendChild());
accessor.setAttribute(ARRAY_ATTRIBUTE_NAME, values);
}
/**
* Remove all children of {@code "OffsetVector"} node.
*/
public void clearOffsetVectors() {
MetadataNodeAccessor accessor = offsetVectors;
if (accessor == null) {
offsetVectors = accessor = new MetadataNodeAccessor(this, "OffsetVectors", "OffsetVector");
}
accessor.removeChildren();
}
/**
* Sets the values of the {@code "SpatialRepresentation"} attributes.
*
* @param centerPoint The value to assign to the {@code "centerPoint"} attribute.
* @param cellGeometry The value to assign to the {@code "cellGeometry"} attribute, or {@code null}.
* @param pointInPixel The value to assign to the {@code "pointInPixel"} attribute, or {@code null}.
*/
public void setSpatialRepresentation(final double[] centerPoint, final CellGeometry cellGeometry,
final PixelOrientation pointInPixel)
{
final MetadataNodeAccessor accessor = new MetadataNodeAccessor(
metadata, GEOTK_FORMAT_NAME, "SpatialRepresentation", null);
accessor.setAttribute("numberOfDimensions", centerPoint.length);
accessor.setAttribute("centerPoint", centerPoint);
accessor.setAttribute("pointInPixel", pointInPixel);
accessor.setAttribute("cellGeometry", cellGeometry);
}
/**
* Convenience methods which set every attributes handled by this accessor.
* This method is the most generic one for the two-dimensional case.
*
* @param gridToCRS The conversion from grid to CRS.
* @param bounds The image bounds.
* @param cellGeometry The value to assign to the {@code "cellGeometry"} attribute.
* @param pointInPixel The value to assign to the {@code "pointInPixel"} attribute.
*/
public void setAll(final AffineTransform gridToCRS, final Rectangle bounds,
final CellGeometry cellGeometry, final PixelOrientation pointInPixel)
{
setGridToCRS(gridToCRS);
setLimits(new int[] {
bounds.x, // X_POSITION
bounds.y}, // Y_POSITION
new int[] {
bounds.x + bounds.width - 1, // X_POSITION
bounds.y + bounds.height - 1}); // Y_POSITION
final double[] centerPoint = new double[] {
bounds.getCenterX(), // X_POSITION
bounds.getCenterY()}; // Y_POSITION
gridToCRS.transform(centerPoint, 0, centerPoint, 0, 1);
/*
* Get an estimation of the envelope size (the diagonal length actually),
* in order to estimate a threshold value for trapping zeros.
*/
Point2D span = new Point2D.Double(
bounds.getWidth(), // X_POSITION
bounds.getHeight()); // Y_POSITION
span = gridToCRS.deltaTransform(span, span);
final double tolerance = Math.hypot(span.getX(), span.getY()) * EPS;
for (int i=0; i<centerPoint.length; i++) {
centerPoint[i] = adjustForRoundingError(centerPoint[i], tolerance);
}
setSpatialRepresentation(centerPoint, cellGeometry, pointInPixel);
}
//
// Methods below this point are convenience specializations for the case
// where only the bounds are specified, instead than the affine transform.
//
/**
* Checks the length of the given array against the expected value.
* In case of mismatch, an {@link IllegalArgumentException} is thrown.
*/
private static void checkDimension(final String name, final int length, final int expected) {
if (length != expected) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.MismatchedDimension_3, name, length, expected));
}
}
/**
* Sets the values of the {@code "SpatialRepresentation"} attributes. This method computes
* the {@code "centerPoint"} attribute from the given {@code "origin"} and {@code "bounds"}
* because this method is typically invoked together with the {@link #setRectifiedGridDomain}
* method.
*
* @param origin The {@code origin} argument given to the {@code setRectifiedGridDomain} method.
* @param bounds The {@code bounds} argument given to the {@code setRectifiedGridDomain} method.
* @param cellGeometry The value to assign to the {@code "cellGeometry"} attribute.
* @param pointInPixel The value to assign to the {@code "pointInPixel"} attribute.
*/
public void setSpatialRepresentation(final double[] origin, final double[] bounds,
final CellGeometry cellGeometry, final PixelOrientation pointInPixel)
{
final int crsDim = origin.length;
checkDimension("bounds", bounds.length, crsDim);
final double[] centerPoint = new double[crsDim];
for (int i=0; i<crsDim; i++) {
final double tolerance = EPS * (bounds[i] - origin[i]);
centerPoint[i] = adjustForRoundingError(0.5 * (origin[i] + bounds[i]), tolerance);
}
setSpatialRepresentation(centerPoint, cellGeometry, pointInPixel);
}
/**
* Sets the values of the {@code "RectifiedGridDomain"} attributes of offset vectors.
* This convenience method invokes the following methods with values computes from
* the arguments:
* <p>
* <ul>
* <li>{@link #setLimits}</li>
* <li>{@link #setOrigin}</li>
* <li>{@link #addOffsetVector}</li>
* </ul>
*
* {@section Grid and CRS dimensions}
* The dimension of the grid (named {@code gridDim} below) is typically equals to the dimension
* of the CRS (named {@code crsDim} below). But in some cases the CRS dimension may be greater
* than the grid dimension (the converse is not allowed however).
* See {@link org.opengis.coverage.grid.RectifiedGrid} for more information.
*
* @param origin
* The coordinate of the pixel at the {@code low} index.
* The length of this array shall be equals to the {@code crsDim}.
* @param bounds
* The coordinate of the pixel at the {@code high} index. The length of this array
* shall be equals to the {@code crsDim}. The ordinate values are often greater than
* {@code origin}, but not always. For example if the direction of the <var>y</var>
* axis in the CRS space has the opposite direction than the corresponding axis in
* the pixel space, then the {@code bounds} value is smaller than {@code origin}.
* @param low
* The smaller pixel index, or {@code null} for an array filled with zeros.
* If non-null, the array length shall be equals to {@code gridDim}.
* @param high
* The largest pixel index, <strong>inclusive</strong>.
* The array length shall be equals to {@code gridDim}.
* @param gridToCrsDim
* If non-null, an array of length {@code gridDim} where, for the grid dimension
* {@code i}, the CRS dimension to use is {@code gridToCrsDim[i]}. For example if
* the first grid dimension is for the <var>y</var> axis in the CRS space and the
* second grid dimension is for the <var>x</var> axis in the CRS space (in other
* words if the axes shall be swapped), then this array shall be {@code {1,0}}.
* A null value is equivalent to an {@code {0,1,2,3...}} array.
* @param pixelCenter
* {@code true} if the {@code origin} and {@code bounds} coordinates map pixel center
* (with both coordinates inclusive), or {@code false} if they are a bounding box
* which contains the totality of the grid.
*/
public void setRectifiedGridDomain(final double[] origin, final double[] bounds, int[] low,
final int[] high, final int[] gridToCrsDim, final boolean pixelCenter)
{
final int crsDim = origin.length;
final int gridDim = high.length;
if (low == null) {
low = new int[gridDim];
}
checkDimension("low", low.length, gridDim);
checkDimension("bounds", bounds.length, crsDim);
if (crsDim < gridDim) {
checkDimension("origin", crsDim, gridDim);
}
if (gridToCrsDim != null) {
checkDimension("gridToCrsDim", gridToCrsDim.length, gridDim);
}
setOrigin(origin);
clearOffsetVectors();
final double[] vector = new double[crsDim];
for (int i=0; i<gridDim; i++) {
final int j = (gridToCrsDim != null) ? gridToCrsDim[i] : i;
int span = high[i] - low[i];
if (!pixelCenter) {
span++;
}
vector[j] = adjustForRoundingError((bounds[i] - origin[i]) / span, 0);
addOffsetVector(vector);
vector[j] = 0;
}
setLimits(low, high);
}
/**
* Convenience method invoking {@link #setSpatialRepresentation setSpatialRepresentation} and
* {@link #setRectifiedGridDomain setRectifiedGridDomain} for a two-dimensional bounding box.
* <p>
* Note that the value of the {@code yBound} parameter can be lower than the value of the
* {@code yOrigin} parameter, in which case the scale factor for the <var>y</var> ordinates
* will be negative.
*
* @param xOrigin The first ordinate of the {@code origin} parameter.
* @param yOrigin The second ordinate of the {@code origin} parameter.
* @param xBound The first ordinate of the {@code bounds} parameter.
* @param yBound The second ordinate of the {@code bounds} parameter.
* @param width The number of pixels along the <var>x</var> axis.
* @param height The number of pixels along the <var>y</var> axis.
* @param cellGeometry The value to assign to the {@code "cellGeometry"} attribute.
* @param pixelCenter {@code true} if the {@code origin} and {@code bounds} coordinates map
* pixel center (with both coordinates inclusive), or {@code false} if they are a
* bounding box which contains the totality of the grid.
*/
public void setAll(final double xOrigin, final double yOrigin, final double xBound,
final double yBound, final int width, final int height, final boolean pixelCenter,
final CellGeometry cellGeometry)
{
final double[] origin = new double[] {xOrigin, yOrigin}; // X_POSITION, Y_POSITION
final double[] bounds = new double[] {xBound, yBound}; // X_POSITION, Y_POSITION
final int[] high = new int[] {width-1, height-1}; // X_POSITION, Y_POSITION
setRectifiedGridDomain(origin, bounds, null, high, null, pixelCenter);
setSpatialRepresentation(origin, bounds, cellGeometry,
pixelCenter ? PixelOrientation.CENTER : PixelOrientation.UPPER_LEFT);
}
/**
* Work around for rounding error, to be invoked only for values resulting from a computation.
*/
private static double adjustForRoundingError(double value, final double tolerance) {
value = MetadataHelper.INSTANCE.adjustForRoundingError(value);
if (Math.abs(value) <= tolerance) {
value = 0;
}
return value;
}
}