/*
* 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.coverage.grid;
import java.awt.Rectangle;
import java.awt.image.RenderedImage;
import org.geotools.geometry.Envelope2D;
import org.geotools.metadata.iso.spatial.PixelTranslation;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.util.Cloneable;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
/**
* Defines a range of two-dimensional grid coverage coordinates. This implementation extends
* {@link Rectangle} for interoperability with Java2D. Note that at the opposite of
* {@link GeneralGridEnvelope}, this class is mutable.
* <p>
* <b>CAUTION:</b>
* ISO 19123 defines {@linkplain #getHigh high} coordinates as <strong>inclusive</strong>.
* We follow this specification for all getters methods, but keep in mind that this is the
* opposite of Java2D usage where {@link Rectangle} maximal values are exclusive.
*
* @since 2.5
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*
* @see GeneralGridEnvelope
*/
public class GridEnvelope2D extends Rectangle implements GridEnvelope, Cloneable {
/**
* For cross-version interoperability.
*/
private static final long serialVersionUID = -3370515914148690059L;
/**
* Creates an initially empty grid envelope.
*/
public GridEnvelope2D() {
}
/**
* Creates a grid envelope initialized to the specified rectangle.
*
* @param rectangle The rectangle to use for initializing this grid envelope.
*/
public GridEnvelope2D(final Rectangle rectangle) {
super(rectangle);
}
/**
* Creates a grid envelope initialized to the specified rectangle.
*
* @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.
*/
public GridEnvelope2D(final int x, final int y, final int width, final int height) {
super(x, y, width, height);
}
/**
* Casts the specified envelope into a grid envelope. This is sometime useful after an
* envelope has been transformed from "real world" coordinates to grid coordinates using the
* {@linkplain org.opengis.coverage.grid.GridGeometry#getGridToCRS grid to CRS} transform.
* The floating point values are rounded toward the nearest integers.
* <p>
* <strong>Notice that highest values are interpreted as non-inclusive</strong>
*
* <p>
* <b>Anchor</b><br>
* According OpenGIS specification, {@linkplain org.opengis.coverage.grid.GridGeometry grid
* geometry} maps pixel's center. But envelopes typically encompass all pixels. This means
* that grid coordinates (0,0) has an envelope starting at (-0.5, -0.5). In order to revert
* back such envelope to a grid envelope, it is necessary to add 0.5 to every coordinates
* (including the maximum value since it is exclusive in a grid envelope). This offset is
* applied only if {@code anchor} is {@link PixelInCell#CELL_CENTER}. Users who don't want
* such offset should specify {@link PixelInCell#CELL_CORNER}.
* <p>
* The convention is specified as a {@link PixelInCell} code instead than the more detailed
* {@link org.opengis.metadata.spatial.PixelOrientation} because the latter is restricted to
* the two-dimensional case while the former can be used for any number of dimensions.
*
* @param envelope
* The envelope to use for initializing this grid envelope.
* @param anchor
* Whatever envelope coordinates map to pixel center or pixel corner. Should be
* {@link PixelInCell#CELL_CENTER} for OGC convention (an offset of 0.5 will be
* added to every envelope coordinate values), or {@link PixelInCell#CELL_CORNER}
* for Java2D/JAI convention (no offset will be added).
* @throws IllegalArgumentException
* If {@code anchor} is not valid.
*
*/
public GridEnvelope2D(final Envelope2D envelope, final PixelInCell anchor)
throws IllegalArgumentException
{
this(envelope,anchor,false);
}
/**
* Casts the specified envelope into a grid envelope. This is sometime useful after an
* envelope has been transformed from "real world" coordinates to grid coordinates using the
* {@linkplain org.opengis.coverage.grid.GridGeometry#getGridToCRS grid to CRS} transform.
* The floating point values are rounded toward the nearest integers.
* <p>
* <b>Note about rounding mode</b><br>
* It would have been possible to round the {@linkplain Envelope#getMinimum minimal value}
* toward {@linkplain Math#floor floor} and the {@linkplain Envelope#getMaximum maximal value}
* toward {@linkplain Math#ceil ceil} in order to make sure that the grid envelope encompass
* fully the envelope - like what Java2D does when converting {@link java.awt.geom.Rectangle2D}
* to {@link Rectangle}). But this approach may increase by 1 or 2 units the image
* {@linkplain RenderedImage#getWidth width} or {@linkplain RenderedImage#getHeight height}. For
* example the range {@code [-0.25 ... 99.75]} (which is exactly 101 units wide) would be casted
* to {@code [-1 ... 100]}, which is 102 units wide. This leads to unexpected results when using
* grid envelope with image operations like "{@link javax.media.jai.operator.AffineDescriptor
* Affine}". For avoiding such changes in size, it is necessary to use the same rounding mode
* for both minimal and maximal values. The selected rounding mode is {@linkplain Math#round
* nearest integer} in this implementation.
* <p>
* <b>Anchor</b><br>
* According OpenGIS specification, {@linkplain org.opengis.coverage.grid.GridGeometry grid
* geometry} maps pixel's center. But envelopes typically encompass all pixels. This means
* that grid coordinates (0,0) has an envelope starting at (-0.5, -0.5). In order to revert
* back such envelope to a grid envelope, it is necessary to add 0.5 to every coordinates
* (including the maximum value since it is exclusive in a grid envelope). This offset is
* applied only if {@code anchor} is {@link PixelInCell#CELL_CENTER}. Users who don't want
* such offset should specify {@link PixelInCell#CELL_CORNER}.
* <p>
* The convention is specified as a {@link PixelInCell} code instead than the more detailed
* {@link org.opengis.metadata.spatial.PixelOrientation} because the latter is restricted to
* the two-dimensional case while the former can be used for any number of dimensions.
*
* @param envelope
* The envelope to use for initializing this grid envelope.
* @param anchor
* Whatever envelope coordinates map to pixel center or pixel corner. Should be
* {@link PixelInCell#CELL_CENTER} for OGC convention (an offset of 0.5 will be
* added to every envelope coordinate values), or {@link PixelInCell#CELL_CORNER}
* for Java2D/JAI convention (no offset will be added).
* @param isHighIncluded
* {@code true} if the envelope maximal values are inclusive, or {@code false} if
* they are exclusive. This argument does not apply to minimal values, which are
* always inclusive.
* @throws IllegalArgumentException
* If {@code anchor} is not valid.
*
*/
public GridEnvelope2D(final Envelope2D envelope, final PixelInCell anchor,
final boolean isHighIncluded)
throws IllegalArgumentException
{
final double offset = PixelTranslation.getPixelTranslation(anchor) + 0.5;
final int dimension = envelope.getDimension();
assert dimension==2;
final int[] index = new int[dimension * 2];
for (int i=0; i<dimension; i++) {
// See "note about conversion of floating point values to integers" in the JavaDoc.
index[i ] = (int) Math.round(envelope.getMinimum(i) + offset);
index[i + dimension] = (int) Math.round(envelope.getMaximum(i) + offset);
}
if (isHighIncluded) {
for (int i=index.length/2; i<index.length; i++) {
index[i]++;
}
}
setLocation(index[0], index[1]);
setSize(index[0+dimension]-index[0], index[1+dimension]-index[1]);
}
/**
* Returns the number of dimensions, which is always 2.
*/
public final int getDimension() {
return 2;
}
/**
* Returns the valid minimum inclusive grid coordinates.
* The sequence contains a minimum value for each dimension of the grid coverage.
*/
public GridCoordinates2D getLow() {
return new GridCoordinates2D(x, y);
}
/**
* Returns the valid maximum <strong>inclusive</strong> grid coordinates.
* The sequence contains a maximum value for each dimension of the grid coverage.
*/
public GridCoordinates2D getHigh() {
return new GridCoordinates2D(x + width - 1, y + height - 1);
}
/**
* Returns the valid minimum inclusive grid coordinate along the specified dimension.
*
* @see #getLow()
*/
public int getLow(final int dimension) {
switch (dimension) {
case 0: return x;
case 1: return y;
default: throw new IndexOutOfBoundsException(GridCoordinates2D.indexOutOfBounds(dimension));
}
}
/**
* Returns the valid maximum <strong>inclusive</strong>
* grid coordinate along the specified dimension.
*
* @see #getHigh()
*/
public int getHigh(final int dimension) {
switch (dimension) {
case 0: return x + width - 1;
case 1: return y + height - 1;
default: throw new IndexOutOfBoundsException(GridCoordinates2D.indexOutOfBounds(dimension));
}
}
/**
* Returns the number of integer grid coordinates along the specified dimension.
* This is equals to {@code getHigh(dimension) - getLow(dimension)}.
*/
public int getSpan(final int dimension) {
switch (dimension) {
case 0: return width;
case 1: return height;
default: throw new IndexOutOfBoundsException(GridCoordinates2D.indexOutOfBounds(dimension));
}
}
// Inherit 'hashCode()' and 'equals' from Rectangle2D, which provides an implementation
// aimed to be common for every Rectangle2D subclasses (not just the Java2D ones) - we
// don't want to change this behavior in order to stay consistent with Java2D.
/**
* Returns a string représentation of this grid envelope. The returned string is
* implementation dependent. It is usually provided for debugging purposes.
*/
@Override
public String toString() {
return GeneralGridEnvelope.toString(this);
}
/**
* Returns a clone of this grid envelope.
*
* @return A clone of this grid envelope.
*/
@Override
public GridEnvelope2D clone() {
return (GridEnvelope2D) super.clone();
}
}