/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.image.io.metadata;
import java.util.Arrays;
import org.geotools.resources.XArray;
import org.opengis.geometry.Envelope;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.metadata.spatial.PixelOrientation;
import org.geotools.util.NumberRange;
import org.geotools.resources.i18n.ErrorKeys;
/**
* A combinaison of {@code <Envelope>} and {@code <RectifiedGrid>} elements in
* {@linkplain GeographicMetadataFormat geographic metadata format}. This class offers similar
* service than {@linkplain Envelope envelope} and {@link GridEnvelope grid range}, except
* that the maximum value for {@linkplain #getOrdinateRange coordinate range} and
* {@linkplain #getGridRange grid range} are inclusives.
* <p>
* The {@code <GridEnvelope>} child element is typically (but not always) initialized
* to the following ranges:
* <ul>
* <li>[0 .. {@linkplain java.awt.image.RenderedImage#getWidth image width} - 1]</li>
* <li>[0 .. {@linkplain java.awt.image.RenderedImage#getHeight image height} - 1]</li>
* </ul>
* </p>
* However <var>n</var>-dimensional grid coverages may contains additional entries.
*
* @since 2.4
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
* @author Cédric Briançon
*/
public class ImageGeometry extends MetadataAccessor {
/**
* The {@code "boundedBy/lowerCorner"} node.
*/
private MetadataAccessor lowerCorner;
/**
* The {@code "boundedBy/upperCorner"} node.
*/
private MetadataAccessor upperCorner;
/**
* The {@code "rectifiedGridDomain/cells"} node.
*/
private MetadataAccessor cells;
/**
* The {@code "rectifiedGridDomain/limits/low"} node.
*/
private MetadataAccessor low;
/**
* The {@code "rectifiedGridDomain/limits/high"} node.
*/
private MetadataAccessor high;
/**
* The {@code "rectifiedGridDomain/localizationGrid"} node.
*/
private MetadataAccessor localizationGrid;
/**
* The {@code "rectifiedGridDomain/pixelOrientation"} node.
*/
private MetadataAccessor pixelOrientation;
/**
* Creates a parser for a grid geometry. This constructor should not be invoked
* directly; use {@link GeographicMetadata#getGeometry} instead.
*
* @param metadata The metadata node.
*/
protected ImageGeometry(final GeographicMetadata metadata) {
super(metadata, "rectifiedGridDomain", null);
}
/**
* Returns the number of dimensions. If the {@link #low} array
* and the cells don't have the same dimension, then a warning is logged and
* the smallest dimension is returned.
* If one of them is empty, the dimension of the oter one is then returned.
*/
public int getDimension() {
final int dim1 = (low != null) ? low.getUserObject(int[].class).length : 0;
final int dim2 = (cells != null) ? cells.childCount() : 0;
if (dim2 == 0) {
return dim1;
}
if (dim1 == 0) {
return dim2;
}
if (dim1 != dim2) {
warning("getDimension", ErrorKeys.MISMATCHED_DIMENSION_$2,
new int[] {dim1, dim2});
}
return Math.min(dim1, dim2);
}
/**
* Returns the range of grid index along the specified dimension. Note that range
* {@linkplain NumberRange#getMinValue minimum value},
* {@linkplain NumberRange#getMaxValue maximum value} or both may be null if no
* {@code "low"} or {@code "high"} attribute were found for the
* {@code "rectifiedGridDomain/limits"} element.
*
* @param dimension The dimension index, from 0 inclusive to {@link #getDimension}
* exclusive.
*/
public NumberRange<Integer> getGridRange(final int dimension) {
final int minimum = (low != null) ? low. getUserObject(int[].class)[dimension] : 0;
final int maximum = (high != null) ? high.getUserObject(int[].class)[dimension] : 0;
return NumberRange.create(minimum, true, maximum, true);
}
/**
* Set the grid range along the specified dimension. If the dimension is greater
* than the current envelope dimension, then this dimension is added.
*
* @param dimension The dimension to set. It can eventually be greater than {@link #getDimension}.
* @param minimum The minimum value along the specified dimension (inclusive).
* @param maximum The maximum value along the specified dimension (<strong>inclusive</strong>).
*/
public void setGridRange(final int dimension, final int minimum, final int maximum) {
int[] lows = getLowAccessor(). getUserObject(int[].class);
int[] highs = getHighAccessor().getUserObject(int[].class);
final int length = dimension + 1;
if (lows == null) {
lows = new int[length];
} else {
final int oldLength = lows.length;
if (length > oldLength) {
lows = XArray.resize(lows, length);
}
}
if (highs == null) {
highs = new int[length];
} else {
final int oldLength = highs.length;
if (length > oldLength) {
highs = XArray.resize(highs, length);
}
}
lows[dimension] = minimum;
highs[dimension] = maximum;
getLowAccessor(). setUserObject(lows);
getHighAccessor().setUserObject(highs);
}
/**
* Returns the range of ordinate values along the specified dimension. Note that range
* {@linkplain NumberRange#getMinValue minimum value},
* {@linkplain NumberRange#getMaxValue maximum value} or both may be null if no
* {@code "lowerCorner"} or {@code "upperCorner"} attribute were found for the
* {@code "boundedBy/Envelope"} element.
*
* @param dimension The dimension index, from 0 inclusive to {@link #getDimension} exclusive.
*/
public NumberRange<Double> getOrdinateRange(final int dimension) {
final double lower = (lowerCorner != null) ?
lowerCorner.getUserObject(double[].class)[dimension] : Double.NaN;
final double upper = (upperCorner != null) ?
upperCorner.getUserObject(double[].class)[dimension] : Double.NaN;
return new NumberRange(Double.class, lower, true, upper, true);
}
/**
* Set the envelope range along the specified dimension. If the dimension is greater
* than the current envelope dimension, then this dimension is added.
*
* @param dimension The dimension to set. It can eventually be greater than {@link #getDimension}.
* @param minimum The minimum value along the specified dimension (inclusive).
* @param maximum The maximum value along the specified dimension (<strong>inclusive</strong>).
*/
public void setOrdinateRange(final int dimension, final double minimum, final double maximum) {
double[] lowers = getLowerCornerAccessor().getUserObject(double[].class);
double[] uppers = getUpperCornerAccessor().getUserObject(double[].class);
final int length = dimension + 1;
if (lowers == null) {
lowers = new double[length];
Arrays.fill(lowers, Double.NaN);
} else {
final int oldLength = lowers.length;
if (length > oldLength) {
lowers = XArray.resize(lowers, length);
Arrays.fill(lowers, oldLength, length, Double.NaN);
}
}
if (uppers == null) {
uppers = new double[length];
Arrays.fill(uppers, Double.NaN);
} else {
final int oldLength = uppers.length;
if (length > oldLength) {
uppers = XArray.resize(uppers, length);
Arrays.fill(uppers, oldLength, length, Double.NaN);
}
}
lowers[dimension] = minimum;
uppers[dimension] = maximum;
getLowerCornerAccessor().setUserObject(lowers);
getUpperCornerAccessor().setUserObject(uppers);
}
/**
* Returns the ordinate values along the specified dimension, or {@code null} if none.
* This method returns a non-null values only if an array of was explicitly specified,
* for example by a call to {@link #setOrdinates}.
*
* @param dimension The dimension index, from 0 inclusive to {@link #getDimension} exclusive.
*/
public double[] getOrdinates(final int dimension) {
if (localizationGrid == null) {
return new double[0];
}
localizationGrid.selectChild(dimension);
return (double[]) getLocalizationGridAccessor().getUserObject();
}
/**
* Set the ordinate values along the specified dimension. The minimum and
* maximum coordinates will be determined from the specified array.
*
* @param dimension The dimension to set, from 0 inclusive to {@link #getDimension} exclusive.
* @param values The coordinate values.
*/
public void setOrdinates(final int dimension, final double[] values) {
double minimum = Double.POSITIVE_INFINITY;
double maximum = Double.NEGATIVE_INFINITY;
if (values != null) {
for (int i=0; i<values.length; i++) {
final double value = values[i];
if (value < minimum) minimum = value;
if (value > maximum) maximum = value;
}
}
setOrdinateRange(dimension, minimum, maximum);
getLocalizationGridAccessor().selectChild(dimension);
getLocalizationGridAccessor().setUserObject(values);
}
/**
* Adds ordinate values for an envelope along a dimension. Invoking this method
* will increase the envelope {@linkplain #getDimension dimension} by one. This method
* may be invoked in replacement of {@link #addOffsetVector} when every cell
* coordinates need to be specified explicitly.
*
* @param minIndex The minimal index value, inclusive. This is usually 0.
* @param values The coordinate values.
*
* @see #addOffsetVector
*/
public void addOrdinates(final int minIndex, final double[] values) {
int[] lows = getLowAccessor(). getUserObject(int[].class);
int[] highs = getHighAccessor().getUserObject(int[].class);
if (lows != null && highs != null) {
final int last = Math.max(lows.length, highs.length);
if (last != lows.length || last != highs.length) {
warning("addOrdinates", ErrorKeys.MISMATCHED_DIMENSION_$2,
new int[]{lows.length, highs.length});
}
lows = XArray.resize(lows, last + 1);
highs = XArray.resize(highs, last + 1);
lows[last] = minIndex;
highs[last] = minIndex + values.length - 1;
} else {
if (lows == null) {
lows = new int[1];
lows[0] = minIndex;
}
if (highs == null) {
highs = new int[1];
highs[0] = minIndex + values.length - 1;
}
}
getLowAccessor(). setUserObject(lows);
getHighAccessor().setUserObject(highs);
setOrdinates(getLocalizationGridAccessor().appendChild(), values);
}
/**
* Returns the offset vector for the specified dimension.
*
* @param dimension The dimension index, from 0 inclusive to {@link #getDimension} exclusive.
*/
public double[] getOffsetVector(final int dimension) {
if (cells == null) {
return new double[0];
}
cells.selectChild(dimension);
return (double[]) getCellsAccessor().getUserObject();
}
/**
* Set the offset vector for the specified dimension.
*
* @param dimension The dimension to set, from 0 inclusive to {@link #getDimension} exclusive.
* @param values The offset values.
*/
public void setOffsetVector(final int dimension, final double[] values) {
final MetadataAccessor cells = getCellsAccessor();
cells.selectChild(dimension);
cells.setUserObject(values);
}
/**
* Adds offset vector values for a new dimension.
*
* @param values The offset values for this new dimension.
*/
public void addOffsetVector(final double[] values) {
setOffsetVector(getCellsAccessor().appendChild(), values);
}
/**
* Returns the point in a pixel corresponding to the Earth location of the pixel,
* or {@code null} if not defined. In the JAI framework, this is typically the
* {@linkplain PixelOrientation#UPPER_LEFT upper left} corner.
* In some OGC specifications, this is often the pixel
* {@linkplain PixelOrientation#CENTER center}.
*
* @param pixelOrientation The pixel orientation (usually {@code "center"},
* {@code "lower left"}, {@code "lower right"}, {@code "upper right"}
* or {@code "upper left"}), or {@code null} if unknown.
*
* @see PixelOrientation
*/
public String getPixelOrientation() {
return (pixelOrientation != null) ?
pixelOrientation.getUserObject(String.class) : null;
}
/**
* Set the pixel orientation to the specified value. The pixel orientation gives
* the point in a pixel corresponding to the Earth location of the pixel. In the
* JAI framework, this is typically the
* {@linkplain PixelOrientation#UPPER_LEFT upper left} corner. In some OGC
* specifications, this is often the pixel {@linkplain PixelOrientation#CENTER center}.
*
* @param pixelOrientation The pixel orientation (usually {@code "center"},
* {@code "lower left"}, {@code "lower right"}, {@code "upper right"}
* or {@code "upper left"}), or {@code null} if unknown.
*
* @see PixelOrientation
*/
public void setPixelOrientation(final String pixelOrientation) {
if (GeographicMetadataFormat.PIXEL_ORIENTATIONS.contains(pixelOrientation)) {
getPixelOrientationAccessor().setUserObject(pixelOrientation);
} else {
warning("setPixelOrientation", ErrorKeys.BAD_PARAMETER_$2, pixelOrientation);
}
}
/**
* Builds a {@linkplain MetadataAccessor cells accessor} if it is
* not already instanciated, and returns it, or return the current one if defined.
*/
private MetadataAccessor getCellsAccessor() {
if (cells == null) {
cells = new MetadataAccessor(metadata, "rectifiedGridDomain/cells", "offsetVector");
}
return cells;
}
/**
* Builds a {@linkplain MetadataAccessor high accessor} if it is not
* already instanciated, and returns it, or return the current one if defined.
*/
private MetadataAccessor getHighAccessor() {
if (high == null) {
high = new MetadataAccessor(metadata, "rectifiedGridDomain/limits/high", null);
}
return high;
}
/**
* Builds a {@linkplain MetadataAccessor localizationGrid accessor} if it is not
* already instanciated, and returns it, or return the current one if defined.
*/
private MetadataAccessor getLocalizationGridAccessor() {
if (localizationGrid == null) {
localizationGrid = new MetadataAccessor(metadata,
"rectifiedGridDomain/localizationGrid", "ordinates");
}
return localizationGrid;
}
/**
* Builds a {@linkplain MetadataAccessor low accessor} if it is not
* already instanciated, and returns it, or return the current one if defined.
*/
private MetadataAccessor getLowAccessor() {
if (low == null) {
low = new MetadataAccessor(metadata, "rectifiedGridDomain/limits/low", null);
}
return low;
}
/**
* Builds a {@linkplain MetadataAccessor lowerCorner accessor} if it is not
* already instanciated, and returns it, or return the current one if defined.
*/
private MetadataAccessor getLowerCornerAccessor() {
if (lowerCorner == null) {
lowerCorner = new MetadataAccessor(metadata, "boundedBy/lowerCorner", null);
}
return lowerCorner;
}
/**
* Builds a {@linkplain MetadataAccessor pixel orientation accessor} if it is not
* already instanciated, and returns it, or return the current one if defined.
*/
private MetadataAccessor getPixelOrientationAccessor() {
if (pixelOrientation == null) {
pixelOrientation = new MetadataAccessor(metadata,
"rectifiedGridDomain/pixelOrientation", null);
}
return pixelOrientation;
}
/**
* Builds a {@linkplain MetadataAccessor upperCorner accessor} if it is not
* already instanciated, and returns it, or return the current one if defined.
*/
private MetadataAccessor getUpperCornerAccessor() {
if (upperCorner == null) {
upperCorner = new MetadataAccessor(metadata, "boundedBy/upperCorner", null);
}
return upperCorner;
}
}