/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-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.geometry; import java.awt.geom.Rectangle2D; import java.io.Serializable; import java.util.Arrays; import javax.measure.unit.Unit; import javax.measure.converter.ConversionException; import org.opengis.util.Cloneable; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.RangeMeaning; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.geometry.MismatchedReferenceSystemException; import org.geotools.referencing.CRS; import org.geotools.resources.Classes; import org.geotools.util.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.geometry.XRectangle2D; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.metadata.iso.spatial.PixelTranslation; /** * A minimum bounding box or rectangle. Regardless of dimension, an {@code Envelope} can be * represented without ambiguity as two {@linkplain DirectPosition direct positions} (coordinate * points). To encode an {@code Envelope}, it is sufficient to encode these two points. * <p> * This particular implementation of {@code Envelope} is said "General" because it uses coordinates * of an arbitrary dimension. * <p> * <strong>Tip:</strong> The metadata package provides a * {@link org.opengis.metadata.extent.GeographicBoundingBox}, which can be used as a kind of * envelope with a coordinate reference system fixed to WGS 84 (EPSG:4326). * * @since 2.0 * @source $URL: * http://svn.osgeo.org/geotools/branches/2.6.x/modules/library/referencing/src/main/java * /org/geotools/geometry/GeneralEnvelope.java $ * @version $Id$ * @author Martin Desruisseaux (IRD) * @author Simone Giannecchini * * @see Envelope2D * @see org.geotools.geometry.jts.ReferencedEnvelope * @see org.opengis.metadata.extent.GeographicBoundingBox */ public class GeneralEnvelope extends AbstractEnvelope implements Cloneable, Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 1752330560227688940L; /** * Minimum and maximum ordinate values. The first half contains minimum ordinates, while the * last half contains maximum ordinates. This layout is convenient for the creation of lower and * upper corner direct positions. * <p> * Consider this reference as final; it is modified by {@link #clone} only. */ private double[] ordinates; /** * The coordinate reference system, or {@code null}. */ private CoordinateReferenceSystem crs; /** * Constructs an empty envelope of the specified dimension. All ordinates are initialized to 0 * and the coordinate reference system is undefined. * * @param dimension * The envelope dimension. */ public GeneralEnvelope(final int dimension) { ordinates = new double[dimension * 2]; } /** * Constructs one-dimensional envelope defined by a range of values. * * @param min * The minimal value. * @param max * The maximal value. */ public GeneralEnvelope(final double min, final double max) { ordinates = new double[] { min, max }; checkCoordinates(ordinates); } /** * Constructs a envelope defined by two positions. * * @param minDP * Minimum ordinate values. * @param maxDP * Maximum ordinate values. * @throws MismatchedDimensionException * if the two positions don't have the same dimension. * @throws IllegalArgumentException * if an ordinate value in the minimum point is not less than or equal to the * corresponding ordinate value in the maximum point. */ public GeneralEnvelope(final double[] minDP, final double[] maxDP) throws IllegalArgumentException { ensureNonNull("minDP", minDP); ensureNonNull("maxDP", maxDP); ensureSameDimension(minDP.length, maxDP.length); ordinates = new double[minDP.length + maxDP.length]; System.arraycopy(minDP, 0, ordinates, 0, minDP.length); System.arraycopy(maxDP, 0, ordinates, minDP.length, maxDP.length); checkCoordinates(ordinates); } /** * Constructs a envelope defined by two positions. The coordinate reference system is inferred * from the supplied direct position. * * @param minDP * Point containing minimum ordinate values. * @param maxDP * Point containing maximum ordinate values. * @throws MismatchedDimensionException * if the two positions don't have the same dimension. * @throws MismatchedReferenceSystemException * if the two positions don't use the same CRS. * @throws IllegalArgumentException * if an ordinate value in the minimum point is not less than or equal to the * corresponding ordinate value in the maximum point. */ public GeneralEnvelope(final GeneralDirectPosition minDP, final GeneralDirectPosition maxDP) throws MismatchedReferenceSystemException, IllegalArgumentException { // Uncomment next lines if Sun fixes RFE #4093999 // ensureNonNull("minDP", minDP); // ensureNonNull("maxDP", maxDP); this(minDP.ordinates, maxDP.ordinates); crs = getCoordinateReferenceSystem(minDP, maxDP); AbstractDirectPosition.checkCoordinateReferenceSystemDimension(crs, ordinates.length / 2); } /** * Constructs an empty envelope with the specified coordinate reference system. All ordinates * are initialized to 0. * * @param crs * The coordinate reference system. * * @since 2.2 */ public GeneralEnvelope(final CoordinateReferenceSystem crs) { // Uncomment next line if Sun fixes RFE #4093999 // ensureNonNull("envelope", envelope); this(crs.getCoordinateSystem().getDimension()); this.crs = crs; } /** * Constructs a new envelope with the same data than the specified envelope. * * @param envelope * The envelope to copy. */ public GeneralEnvelope(final Envelope envelope) { ensureNonNull("envelope", envelope); if (envelope instanceof GeneralEnvelope) { final GeneralEnvelope e = (GeneralEnvelope) envelope; ordinates = e.ordinates.clone(); crs = e.crs; } else { crs = envelope.getCoordinateReferenceSystem(); final int dimension = envelope.getDimension(); ordinates = new double[2 * dimension]; for (int i = 0; i < dimension; i++) { ordinates[i] = envelope.getMinimum(i); ordinates[i + dimension] = envelope.getMaximum(i); } checkCoordinates(ordinates); } } /** * Constructs a new envelope with the same data than the specified geographic bounding box. The * coordinate reference system is set to {@linkplain DefaultGeographicCRS#WGS84 WGS84}. * * @param box * The bounding box to copy. * * @since 2.4 */ public GeneralEnvelope(final GeographicBoundingBox box) { ensureNonNull("box", box); ordinates = new double[] { box.getWestBoundLongitude(), box.getSouthBoundLatitude(), box.getEastBoundLongitude(), box.getNorthBoundLatitude() }; crs = DefaultGeographicCRS.WGS84; } /** * Constructs two-dimensional envelope defined by a {@link Rectangle2D}. The coordinate * reference system is initially undefined. * * @param rect * The rectangle to copy. */ public GeneralEnvelope(final Rectangle2D rect) { ensureNonNull("rect", rect); ordinates = new double[] { rect.getMinX(), rect.getMinY(), rect.getMaxX(), rect.getMaxY() }; checkCoordinates(ordinates); } /** * Creates an envelope for a grid range transformed to an envelope using the specified math * transform. The <cite>grid to CRS</cite> transform should map either the * {@linkplain PixelInCell#CELL_CENTER cell center} (as in OGC convention) or * {@linkplain PixelInCell#CELL_CORNER cell corner} (as in Java2D/JAI convention) depending on * the {@code anchor} value. This constructor creates an envelope containing entirely all pixels * on a <cite>best effort</cite> basis - usually accurate for affine transforms. * <p> * <b>Note:</b> The convention is specified as a {@link PixelInCell} code instead than the more * detailled {@link org.opengis.metadata.spatial.PixelOrientation}, because the later is * restricted to the two-dimensional case while the former can be used for any number of * dimensions. * * @param gridRange * The grid range. * @param anchor * Whatever grid range coordinates map to pixel center or pixel corner. * @param gridToCRS * The transform (usually affine) from grid range to the envelope CRS. * @param crs * The envelope CRS, or {@code null} if unknow. * * @throws MismatchedDimensionException * If one of the supplied object doesn't have a dimension compatible with the other * objects. * @throws IllegalArgumentException * if an argument is illegal for some other reason, including failure to use the * provided math transform. * * @since 2.3 * * @see org.geotools.coverage.grid.GeneralGridEnvelope#GeneralGridEnvelope(Envelope,PixelInCell,boolean) */ public GeneralEnvelope(final GridEnvelope gridRange, final PixelInCell anchor, final MathTransform gridToCRS, final CoordinateReferenceSystem crs) throws IllegalArgumentException { ensureNonNull("gridRange", gridRange); ensureNonNull("gridToCRS", gridToCRS); final int dimRange = gridRange.getDimension(); final int dimSource = gridToCRS.getSourceDimensions(); final int dimTarget = gridToCRS.getTargetDimensions(); ensureSameDimension(dimRange, dimSource); ensureSameDimension(dimRange, dimTarget); ordinates = new double[dimSource * 2]; final double offset = PixelTranslation.getPixelTranslation(anchor) + 0.5; for (int i = 0; i < dimSource; i++) { /* * According OpenGIS specification, GridGeometry maps pixel's center. We want a bounding * box for all pixels, not pixel's centers. Offset by 0.5 (use -0.5 for maximum too, not * +0.5, since maximum is exclusive). * * Note: the offset of 1 after getHigh(i) is because high values are inclusive according * ISO specification, while our algorithm and Java usage expect exclusive values. */ setRange(i, gridRange.getLow(i) - offset, gridRange.getHigh(i) - (offset - 1)); } final GeneralEnvelope transformed; try { transformed = CRS.transform(gridToCRS, this); } catch (TransformException exception) { throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_TRANSFORM_$1, Classes .getClass(gridToCRS)), exception); } assert transformed.ordinates.length == this.ordinates.length; System.arraycopy(transformed.ordinates, 0, this.ordinates, 0, ordinates.length); setCoordinateReferenceSystem(crs); } /** * Makes sure an argument is non-null. * * @param name * Argument name. * @param object * User argument. * @throws InvalidParameterValueException * if {@code object} is 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 the specified dimensions are identical. */ private static void ensureSameDimension(final int dim1, final int dim2) throws MismatchedDimensionException { if (dim1 != dim2) { throw new MismatchedDimensionException(Errors.format(ErrorKeys.MISMATCHED_DIMENSION_$2, dim1, dim2)); } } /** * Checks if ordinate values in the minimum point are less than or equal to the corresponding * ordinate value in the maximum point. * <p> * This code will recognize the following exceptions: * <ul> * <li>ordinates encoding isNil</li> * <li>ordinates encoding isEmpty</li> * </ul> * @throws IllegalArgumentException * if an ordinate value in the minimum point is not less than or equal to the * corresponding ordinate value in the maximum point. */ private static void checkCoordinates(final double[] ordinates) throws IllegalArgumentException { if( isNilCoordinates( ordinates )){ return; // null ordinates are okay } if( isEmptyOrdinates(ordinates)){ return; // empty ordinates are also a valid encoding.... } final int dimension = ordinates.length / 2; for (int i = 0; i < dimension; i++) { if (!(ordinates[i] <= ordinates[dimension + i])) { // Use '!' in order to catch 'NaN'. throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ENVELOPE_ORDINATE_$1, i)); } } } /** * Returns the coordinate reference system in which the coordinates are given. * * @return The coordinate reference system, or {@code null}. */ public final CoordinateReferenceSystem getCoordinateReferenceSystem() { assert crs == null || crs.getCoordinateSystem().getDimension() == getDimension(); return crs; } /** * Sets the coordinate reference system in which the coordinate are given. This method * <strong>do not</strong> reproject the envelope, and do not check if the envelope is contained * in the new domain of validity. The later can be enforced by a call to {@link #normalize}. * * @param crs * The new coordinate reference system, or {@code null}. * @throws MismatchedDimensionException * if the specified CRS doesn't have the expected number of dimensions. */ public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) throws MismatchedDimensionException { AbstractDirectPosition.checkCoordinateReferenceSystemDimension(crs, getDimension()); this.crs = crs; } /** * Restricts this envelope to the CS or CRS * {@linkplain CoordinateReferenceSystem#getDomainOfValidity domain of validity}. This method * performs two steps: * * <ol> * <li> * <p> * First, it ensures that the envelope is contained in the {@linkplain CoordinateSystem * coordinate system} domain. Out of range ordinates are validated in a way that depends on the * {@linkplain CoordinateSystemAxis#getRangeMeaning range meaning}: * <ul> * <li>If {@linkplain RangeMeaning#EXACT EXACT} (typically <em>latitudes</em> ordinates), values * greater than the {@linkplain CoordinateSystemAxis#getMaximumValue maximum value} are replaced * by the maximum, and values smaller than the {@linkplain CoordinateSystemAxis#getMinimumValue * minimum value} are replaced by the minimum.</li> * * <li>If {@linkplain RangeMeaning#WRAPAROUND WRAPAROUND} (typically <em>longitudes</em> * ordinates), a multiple of the range (e.g. 360° for longitudes) is added or subtracted. If a * value stay out of range after this correction, then the ordinates are set to the full [ * {@linkplain CoordinateSystemAxis#getMinimumValue minimum} ... * {@linkplain CoordinateSystemAxis#getMaximumValue maximum}] range. * * <blockquote> <b>Example:</b> [185° ... 190°] of longitude is equivalent to [-175° ... * -170°]. But [175° ... 185°] would be equivalent to [175° ... -175°], which is likely to * mislead most users of {@link Envelope} since the lower bounds is numerically greater than the * upper bounds. Reordering as [-175° ... 175°] would interchange the meaning of what is * "inside" and "outside" the envelope. So this implementation conservatively expands the range * to [-180° ... 180°] in order to ensure that the validated envelope fully contains the * original envelope. </blockquote></li> * </ul> * </p> * </li> * <li> * <p> * Second and only if {@code crsDomain} is {@code true}, the envelope normalized in the previous * step is intersected with the CRS {@linkplain CoordinateReferenceSystem#getDomainOfValidity * domain of validity}, if any. * </p> * </li> * </ol> * * @param crsDomain * {@code true} if the envelope should be restricted to the CRS domain in addition of * the CS domain. * @return {@code true} if this envelope has been modified, or {@code false} if no change was * done. * * @since 2.5 */ public boolean normalize(final boolean crsDomain) { boolean changed = false; if (crs != null) { final int dimension = ordinates.length / 2; final CoordinateSystem cs = crs.getCoordinateSystem(); for (int i = 0; i < dimension; i++) { final int j = i + dimension; final CoordinateSystemAxis axis = cs.getAxis(i); final double minimum = axis.getMinimumValue(); final double maximum = axis.getMaximumValue(); final RangeMeaning rm = axis.getRangeMeaning(); if (RangeMeaning.EXACT.equals(rm)) { if (ordinates[i] < minimum) { ordinates[i] = minimum; changed = true; } if (ordinates[j] > maximum) { ordinates[j] = maximum; changed = true; } } else if (RangeMeaning.WRAPAROUND.equals(rm)) { final double length = maximum - minimum; if (length > 0 && length < Double.POSITIVE_INFINITY) { final double offset = Math.floor((ordinates[i] - minimum) / length) * length; if (offset != 0) { ordinates[i] -= offset; ordinates[j] -= offset; changed = true; } if (ordinates[j] > maximum) { ordinates[i] = minimum; // See method Javadoc ordinates[j] = maximum; changed = true; } } } } if (crsDomain) { final Envelope domain = CRS.getEnvelope(crs); if (domain != null) { final CoordinateReferenceSystem domainCRS = domain .getCoordinateReferenceSystem(); if (domainCRS == null) { intersect(domain); } else { /* * The domain may have fewer dimensions than this envelope (typically only * the ones relative to horizontal dimensions). We can rely on directions * for matching axis since CRS.getEnvelope(crs) should have transformed the * domain to this envelope CRS. */ final CoordinateSystem domainCS = domainCRS.getCoordinateSystem(); final int domainDimension = domainCS.getDimension(); for (int i = 0; i < domainDimension; i++) { final double minimum = domain.getMinimum(i); final double maximum = domain.getMaximum(i); final AxisDirection direction = domainCS.getAxis(i).getDirection(); for (int j = 0; j < dimension; j++) { if (direction.equals(cs.getAxis(j).getDirection())) { final int k = j + dimension; if (ordinates[j] < minimum) ordinates[j] = minimum; if (ordinates[k] > maximum) ordinates[k] = maximum; } } } } } } } return changed; } /** * Returns the number of dimensions. */ public final int getDimension() { return ordinates.length / 2; } /** * A coordinate position consisting of all the {@linkplain #getMinimum minimal ordinates} for * each dimension for all points within the {@code Envelope}. * * @return The lower corner. */ @Override public DirectPosition getLowerCorner() { final int dim = ordinates.length / 2; final GeneralDirectPosition position = new GeneralDirectPosition(dim); System.arraycopy(ordinates, 0, position.ordinates, 0, dim); position.setCoordinateReferenceSystem(crs); return position; } /** * A coordinate position consisting of all the {@linkplain #getMaximum maximal ordinates} for * each dimension for all points within the {@code Envelope}. * * @return The upper corner. */ @Override public DirectPosition getUpperCorner() { final int dim = ordinates.length / 2; final GeneralDirectPosition position = new GeneralDirectPosition(dim); System.arraycopy(ordinates, dim, position.ordinates, 0, dim); position.setCoordinateReferenceSystem(crs); return position; } /** * A coordinate position consisting of all the {@linkplain #getCenter(int) middle ordinates} for * each dimension for all points within the {@code Envelope}. * * @return The center coordinates. * * @since 2.3 * * @deprecated Renamed as {@link #getMedian(}. */ @Deprecated public DirectPosition getCenter() { return getMedian(); } /** * A coordinate position consisting of all the {@linkplain #getCenter(int) middle ordinates} for * each dimension for all points within the {@code Envelope}. * * @return The median coordinates. * * @since 2.5 */ public DirectPosition getMedian() { final GeneralDirectPosition position = new GeneralDirectPosition(ordinates.length / 2); for (int i = position.ordinates.length; --i >= 0;) { position.ordinates[i] = getMedian(i); } position.setCoordinateReferenceSystem(crs); return position; } /** * Creates an exception for an index out of bounds. */ private static IndexOutOfBoundsException indexOutOfBounds(final int dimension) { return new IndexOutOfBoundsException(Errors.format(ErrorKeys.INDEX_OUT_OF_BOUNDS_$1, dimension)); } /** * Returns the minimal ordinate along the specified dimension. * * @param dimension * The dimension to query. * @return The minimal ordinate value along the given dimension. * @throws IndexOutOfBoundsException * If the given index is out of bounds. */ public final double getMinimum(final int dimension) throws IndexOutOfBoundsException { if (dimension < ordinates.length / 2) { return ordinates[dimension]; } else { throw indexOutOfBounds(dimension); } } /** * Returns the maximal ordinate along the specified dimension. * * @param dimension * The dimension to query. * @return The maximal ordinate value along the given dimension. * @throws IndexOutOfBoundsException * If the given index is out of bounds. */ public final double getMaximum(final int dimension) throws IndexOutOfBoundsException { if (dimension >= 0) { return ordinates[dimension + ordinates.length / 2]; } else { throw indexOutOfBounds(dimension); } } /** * Returns the center ordinate along the specified dimension. * * @param dimension * The dimension to query. * @return The mid ordinate value along the given dimension. * * @deprecated Renamed as {@link #getMedian(int)}. */ @Deprecated public final double getCenter(final int dimension) { return getMedian(dimension); } /** * Returns the median ordinate along the specified dimension. The result should be equals (minus * rounding error) to <code>({@linkplain #getMaximum getMaximum}(dimension) - * {@linkplain #getMinimum getMinimum}(dimension)) / 2</code>. * * @param dimension * The dimension to query. * @return The mid ordinate value along the given dimension. * @throws IndexOutOfBoundsException * If the given index is out of bounds. */ public final double getMedian(final int dimension) throws IndexOutOfBoundsException { return 0.5 * (ordinates[dimension] + ordinates[dimension + ordinates.length / 2]); } /** * Returns the envelope length along the specified dimension. This length is equals to the * maximum ordinate minus the minimal ordinate. * * @param dimension * The dimension to query. * @return The difference along maximal and minimal ordinates in the given dimension. * * @deprecated Renamed as {@link #getSpan(int)}. */ @Deprecated public final double getLength(final int dimension) { return getSpan(dimension); } /** * Returns the envelope span (typically width or height) along the specified dimension. The * result should be equals (minus rounding error) to <code>{@linkplain #getMaximum * getMaximum}(dimension) - {@linkplain #getMinimum getMinimum}(dimension)</code>. * * @param dimension * The dimension to query. * @return The difference along maximal and minimal ordinates in the given dimension. * @throws IndexOutOfBoundsException * If the given index is out of bounds. */ public final double getSpan(final int dimension) throws IndexOutOfBoundsException { return ordinates[dimension + ordinates.length / 2] - ordinates[dimension]; } /** * Returns the envelope length along the specified dimension, in terms of the given units. * * @param dimension * The dimension to query. * @param unit * The unit for the return value. * @return The length in terms of the given unit. * @throws ConversionException * if the length can't be converted to the specified units. * * @since 2.2 * * @deprecated Renamed as {@link #getSpan(int,Unit)}. */ @Deprecated public double getLength(final int dimension, final Unit<?> unit) throws ConversionException { return getSpan(dimension, unit); } /** * Returns the envelope span along the specified dimension, in terms of the given units. * * @param dimension * The dimension to query. * @param unit * The unit for the return value. * @return The span in terms of the given unit. * @throws IndexOutOfBoundsException * If the given index is out of bounds. * @throws ConversionException * if the length can't be converted to the specified units. * * @since 2.5 */ public double getSpan(final int dimension, final Unit<?> unit) throws IndexOutOfBoundsException, ConversionException { double value = getSpan(dimension); if (crs != null) { final Unit<?> source = crs.getCoordinateSystem().getAxis(dimension).getUnit(); if (source != null) { value = source.getConverterTo(unit).convert(value); } } return value; } /** * Sets the envelope's range along the specified dimension. * * @param dimension * The dimension to set. * @param minimum * The minimum value along the specified dimension. * @param maximum * The maximum value along the specified dimension. * @throws IndexOutOfBoundsException * If the given index is out of bounds. */ public void setRange(final int dimension, double minimum, double maximum) throws IndexOutOfBoundsException { if (minimum > maximum) { // Make an empty envelope (min == max) // while keeping it legal (min <= max). minimum = maximum = 0.5 * (minimum + maximum); } if (dimension >= 0) { // An exception will be thrown before any change if 'dimension' is out of range. ordinates[dimension + ordinates.length / 2] = maximum; ordinates[dimension] = minimum; } else { throw indexOutOfBounds(dimension); } } /** * Sets the envelope to the specified values, which must be the lower corner coordinates * followed by upper corner coordinates. The number of arguments provided shall be twice this * {@linkplain #getDimension envelope dimension}, and minimum shall not be greater than maximum. * <p> * <b>Example:</b> (<var>x</var><sub>min</sub>, <var>y</var><sub>min</sub>, * <var>z</var><sub>min</sub>, <var>x</var><sub>max</sub>, <var>y</var><sub>max</sub>, * <var>z</var><sub>max</sub>) * * @param ordinates * The new ordinate values. * * @since 2.5 */ public void setEnvelope(final double... ordinates) { if ((ordinates.length & 1) != 0) { throw new IllegalArgumentException(Errors.format(ErrorKeys.ODD_ARRAY_LENGTH_$1, ordinates.length)); } final int dimension = ordinates.length >>> 1; final int check = this.ordinates.length >>> 1; if (dimension != check) { throw new MismatchedDimensionException(Errors.format(ErrorKeys.MISMATCHED_DIMENSION_$3, "ordinates", dimension, check)); } checkCoordinates(ordinates); System.arraycopy(ordinates, 0, this.ordinates, 0, ordinates.length); } /** * Sets this envelope to the same coordinate values than the specified envelope. * * @param envelope * The new envelope to copy coordinates from. * @throws MismatchedDimensionException * if the specified envelope doesn't have the expected number of dimensions. * * @since 2.2 */ public void setEnvelope(final GeneralEnvelope envelope) throws MismatchedDimensionException { ensureNonNull("envelope", envelope); AbstractDirectPosition.ensureDimensionMatch("envelope", envelope.getDimension(), getDimension()); System.arraycopy(envelope.ordinates, 0, ordinates, 0, ordinates.length); if (envelope.crs != null) { crs = envelope.crs; assert crs.getCoordinateSystem().getDimension() == getDimension() : crs; assert !envelope.getClass().equals(getClass()) || equals(envelope) : envelope; } } /** * Sets the lower corner to {@linkplain Double#NEGATIVE_INFINITY negative infinity} and the * upper corner to {@linkplain Double#POSITIVE_INFINITY positive infinity}. The * {@linkplain #getCoordinateReferenceSystem coordinate reference system} (if any) stay * unchanged. * * @since 2.2 */ public void setToInfinite() { final int mid = ordinates.length / 2; Arrays.fill(ordinates, 0, mid, Double.NEGATIVE_INFINITY); Arrays.fill(ordinates, mid, ordinates.length, Double.POSITIVE_INFINITY); assert isInfinite() : this; } /** * Returns {@code true} if at least one ordinate has an {@linkplain Double#isInfinite infinite} * value. * * @return {@code true} if this envelope has infinite value. * * @since 2.2 */ public boolean isInfinite() { for (int i = 0; i < ordinates.length; i++) { if (Double.isInfinite(ordinates[i])) { return true; } } return false; } /** * Sets all ordinate values to {@linkplain Double#NaN NaN}. The * {@linkplain #getCoordinateReferenceSystem coordinate reference system} (if any) stay * unchanged. * * @since 2.2 */ public void setToNull() { Arrays.fill(ordinates, Double.NaN); assert isNull() : this; } /** * Returns {@code false} if at least one ordinate value is not {@linkplain Double#NaN NaN}. The * {@code isNull()} check is a little bit different than {@link #isEmpty()} since it returns * {@code false} for a partially initialized envelope, while {@code isEmpty()} returns {@code * false} only after all dimensions have been initialized. More specifically, the following * rules apply: * <p> * <ul> * <li>If <code>isNull() == true</code>, then <code>{@linkplain #isEmpty()} == true</code></li> * <li>If <code>{@linkplain #isEmpty()} == false</code>, then <code>isNull() == false</code></li> * <li>The converse of the above-cited rules are not always true.</li> * </ul> * * @return {@code true} if this envelope has NaN values. * * @since 2.2 */ public boolean isNull() { if (!isNilCoordinates(ordinates)) { return false; } assert isEmpty() : this; return true; } /** * Check if the ordinates indicate a "nil" envelope. * @param ordinates * @return * @throws IllegalArgumentException */ private static boolean isNilCoordinates(final double[] ordinates) throws IllegalArgumentException { for (int i = 0; i < ordinates.length; i++) { if (!Double.isNaN(ordinates[i])) { return false; } } return true; } /** * Determines whether or not this envelope is empty. An envelope is non-empty only if it has at * least one {@linkplain #getDimension dimension}, and the {@linkplain #getLength length} is * greater than 0 along all dimensions. Note that a non-empty envelope is always non- * {@linkplain #isNull null}, but the converse is not always true. * * @return {@code true} if this envelope is empty. */ public boolean isEmpty() { if( isEmptyOrdinates(ordinates)){ return true; } assert !isNull() : this; // JG I worry that this is circular return false; } /** * Static method used to recognize an empty encoding of ordindates * @param ordinates * @return true of the ordinates indicate an empty envelope * @see #isEmpty() */ private static boolean isEmptyOrdinates( double ordinates[] ){ final int dimension = ordinates.length / 2; if (dimension == 0) { return true; } for (int i = 0; i < dimension; i++) { if (!(ordinates[i] < ordinates[i + dimension])) { // Use '!' in order to catch NaN return true; } } return false; } /** * Returns {@code true} if at least one of the specified CRS is null, or both CRS are equals. * This special processing for {@code null} values is different from the usual contract of an * {@code equals} method, but allow to handle the case where the CRS is unknown. */ private static boolean equalsIgnoreMetadata(final CoordinateReferenceSystem crs1, final CoordinateReferenceSystem crs2) { return crs1 == null || crs2 == null || CRS.equalsIgnoreMetadata(crs1, crs2); } /** * Adds a point to this envelope. The resulting envelope is the smallest envelope that contains * both the original envelope and the specified point. After adding a point, a call to * {@link #contains} with the added point as an argument will return {@code true}, except if one * of the point's ordinates was {@link Double#NaN} (in which case the corresponding ordinate * have been ignored). * <p> * This method assumes that the specified point uses the same CRS than this envelope. For * performance reason, it will no be verified unless J2SE assertions are enabled. * * @param position * The point to add. * @throws MismatchedDimensionException * if the specified point doesn't have the expected dimension. */ public void add(final DirectPosition position) throws MismatchedDimensionException { ensureNonNull("position", position); final int dim = ordinates.length / 2; AbstractDirectPosition.ensureDimensionMatch("position", position.getDimension(), dim); assert equalsIgnoreMetadata(crs, position.getCoordinateReferenceSystem()) : position; for (int i = 0; i < dim; i++) { final double value = position.getOrdinate(i); if (value < ordinates[i]) ordinates[i] = value; if (value > ordinates[i + dim]) ordinates[i + dim] = value; } assert isEmpty() || contains(position); } /** * Adds an envelope object to this envelope. The resulting envelope is the union of the two * {@code Envelope} objects. * <p> * This method assumes that the specified envelope uses the same CRS than this envelope. For * performance reason, it will no be verified unless J2SE assertions are enabled. * * @param envelope * the {@code Envelope} to add to this envelope. * @throws MismatchedDimensionException * if the specified envelope doesn't have the expected dimension. */ public void add(final Envelope envelope) throws MismatchedDimensionException { ensureNonNull("envelope", envelope); final int dim = ordinates.length / 2; AbstractDirectPosition.ensureDimensionMatch("envelope", envelope.getDimension(), dim); assert equalsIgnoreMetadata(crs, envelope.getCoordinateReferenceSystem()) : envelope; for (int i = 0; i < dim; i++) { final double min = envelope.getMinimum(i); final double max = envelope.getMaximum(i); if (min < ordinates[i]) ordinates[i] = min; if (max > ordinates[i + dim]) ordinates[i + dim] = max; } assert isEmpty() || contains(envelope, true); } /** * Tests if a specified coordinate is inside the boundary of this envelope. * <p> * This method assumes that the specified point uses the same CRS than this envelope. For * performance reason, it will no be verified unless J2SE assertions are enabled. * * @param position * The point to text. * @return {@code true} if the specified coordinates are inside the boundary of this envelope; * {@code false} otherwise. * @throws MismatchedDimensionException * if the specified point doesn't have the expected dimension. */ public boolean contains(final DirectPosition position) throws MismatchedDimensionException { ensureNonNull("position", position); final int dim = ordinates.length / 2; AbstractDirectPosition.ensureDimensionMatch("point", position.getDimension(), dim); assert equalsIgnoreMetadata(crs, position.getCoordinateReferenceSystem()) : position; for (int i = 0; i < dim; i++) { final double value = position.getOrdinate(i); if (!(value >= ordinates[i])) return false; if (!(value <= ordinates[i + dim])) return false; // Use '!' in order to take 'NaN' in account. } return true; } /** * Returns {@code true} if this envelope completly encloses the specified envelope. If one or * more edges from the specified envelope coincide with an edge from this envelope, then this * method returns {@code true} only if {@code edgesInclusive} is {@code true}. * <p> * This method assumes that the specified envelope uses the same CRS than this envelope. For * performance reason, it will no be verified unless J2SE assertions are enabled. * * @param envelope * The envelope to test for inclusion. * @param edgesInclusive * {@code true} if this envelope edges are inclusive. * @return {@code true} if this envelope completly encloses the specified one. * @throws MismatchedDimensionException * if the specified envelope doesn't have the expected dimension. * * @see #intersects(Envelope, boolean) * @see #equals(Envelope, double) * * @since 2.2 */ public boolean contains(final Envelope envelope, final boolean edgesInclusive) throws MismatchedDimensionException { ensureNonNull("envelope", envelope); final int dim = ordinates.length / 2; AbstractDirectPosition.ensureDimensionMatch("envelope", envelope.getDimension(), dim); assert equalsIgnoreMetadata(crs, envelope.getCoordinateReferenceSystem()) : envelope; for (int i = 0; i < dim; i++) { double inner = envelope.getMinimum(i); double outer = ordinates[i]; if (!(edgesInclusive ? inner >= outer : inner > outer)) { // ! is for catching NaN. return false; } inner = envelope.getMaximum(i); outer = ordinates[i + dim]; if (!(edgesInclusive ? inner <= outer : inner < outer)) { // ! is for catching NaN. return false; } } assert intersects(envelope, edgesInclusive); return true; } /** * Returns {@code true} if this envelope intersects the specified envelope. If one or more edges * from the specified envelope coincide with an edge from this envelope, then this method * returns {@code true} only if {@code edgesInclusive} is {@code true}. * <p> * This method assumes that the specified envelope uses the same CRS than this envelope. For * performance reason, it will no be verified unless J2SE assertions are enabled. * * @param envelope * The envelope to test for intersection. * @param edgesInclusive * {@code true} if this envelope edges are inclusive. * @return {@code true} if this envelope intersects the specified one. * @throws MismatchedDimensionException * if the specified envelope doesn't have the expected dimension. * * @see #contains(Envelope, boolean) * @see #equals(Envelope, double) * * @since 2.2 */ public boolean intersects(final Envelope envelope, final boolean edgesInclusive) throws MismatchedDimensionException { ensureNonNull("envelope", envelope); final int dim = ordinates.length / 2; AbstractDirectPosition.ensureDimensionMatch("envelope", envelope.getDimension(), dim); assert equalsIgnoreMetadata(crs, envelope.getCoordinateReferenceSystem()) : envelope; for (int i = 0; i < dim; i++) { double inner = envelope.getMaximum(i); double outer = ordinates[i]; if (!(edgesInclusive ? inner >= outer : inner > outer)) { // ! is for catching NaN. return false; } inner = envelope.getMinimum(i); outer = ordinates[i + dim]; if (!(edgesInclusive ? inner <= outer : inner < outer)) { // ! is for catching NaN. return false; } } return true; } /** * Sets this envelope to the intersection if this envelope with the specified one. * <p> * This method assumes that the specified envelope uses the same CRS than this envelope. For * performance reason, it will no be verified unless J2SE assertions are enabled. * * @param envelope * the {@code Envelope} to intersect to this envelope. * @throws MismatchedDimensionException * if the specified envelope doesn't have the expected dimension. */ public void intersect(final Envelope envelope) throws MismatchedDimensionException { ensureNonNull("envelope", envelope); final int dim = ordinates.length / 2; AbstractDirectPosition.ensureDimensionMatch("envelope", envelope.getDimension(), dim); assert equalsIgnoreMetadata(crs, envelope.getCoordinateReferenceSystem()) : envelope; for (int i = 0; i < dim; i++) { double min = Math.max(ordinates[i], envelope.getMinimum(i)); double max = Math.min(ordinates[i + dim], envelope.getMaximum(i)); if (min > max) { // Make an empty envelope (min==max) // while keeping it legal (min<=max). min = max = 0.5 * (min + max); } ordinates[i] = min; ordinates[i + dim] = max; } } /** * Returns a new envelope that encompass only some dimensions of this envelope. This method copy * this envelope's ordinates into a new envelope, beginning at dimension <code>lower</code> and * extending to dimension <code>upper-1</code>. Thus the dimension of the subenvelope is * <code>upper-lower</code>. * * @param lower * The first dimension to copy, inclusive. * @param upper * The last dimension to copy, exclusive. * @return The subenvelope. * @throws IndexOutOfBoundsException * if an index is out of bounds. */ public GeneralEnvelope getSubEnvelope(final int lower, final int upper) throws IndexOutOfBoundsException { final int curDim = ordinates.length / 2; final int newDim = upper - lower; if (lower < 0 || lower > curDim) { throw new IndexOutOfBoundsException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "lower", lower)); } if (newDim < 0 || upper > curDim) { throw new IndexOutOfBoundsException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "upper", upper)); } final GeneralEnvelope envelope = new GeneralEnvelope(newDim); System.arraycopy(ordinates, lower, envelope.ordinates, 0, newDim); System.arraycopy(ordinates, lower + curDim, envelope.ordinates, newDim, newDim); return envelope; } /** * Returns a new envelope with the same values than this envelope minus the specified range of * dimensions. * * @param lower * The first dimension to omit, inclusive. * @param upper * The last dimension to omit, exclusive. * @return The subenvelope. * @throws IndexOutOfBoundsException * if an index is out of bounds. */ public GeneralEnvelope getReducedEnvelope(final int lower, final int upper) throws IndexOutOfBoundsException { final int curDim = ordinates.length / 2; final int rmvDim = upper - lower; if (lower < 0 || lower > curDim) { throw new IndexOutOfBoundsException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "lower", lower)); } if (rmvDim < 0 || upper > curDim) { throw new IndexOutOfBoundsException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "upper", upper)); } final GeneralEnvelope envelope = new GeneralEnvelope(curDim - rmvDim); System.arraycopy(ordinates, 0, envelope.ordinates, 0, lower); System.arraycopy(ordinates, lower, envelope.ordinates, upper, curDim - upper); return envelope; } /** * Returns a {@link Rectangle2D} with the same bounds as this {@code Envelope}. This is a * convenience method for interoperability with Java2D. * * @return This envelope as a twp-dimensional rectangle. * @throws IllegalStateException * if this envelope is not two-dimensional. */ public Rectangle2D toRectangle2D() throws IllegalStateException { if (ordinates.length == 4) { return XRectangle2D.createFromExtremums(ordinates[0], ordinates[1], ordinates[2], ordinates[3]); } else { throw new IllegalStateException(Errors.format(ErrorKeys.NOT_TWO_DIMENSIONAL_$1, getDimension())); } } /** * Returns a hash value for this envelope. */ @Override public int hashCode() { int code = Arrays.hashCode(ordinates); if (crs != null) { code += crs.hashCode(); } assert code == super.hashCode(); return code; } /** * Compares the specified object with this envelope for equality. */ @Override public boolean equals(final Object object) { if (object != null && object.getClass().equals(getClass())) { final GeneralEnvelope that = (GeneralEnvelope) object; return Arrays.equals(this.ordinates, that.ordinates) && Utilities.equals(this.crs, that.crs); } return false; } /** * Compares to the specified envelope for equality up to the specified tolerance value. The * tolerance value {@code eps} can be either relative to the {@linkplain #getLength envelope * length} along each dimension or can be an absolute value (as for example some ground * resolution of a {@linkplain org.opengis.coverage.grid.GridCoverage grid coverage}). * <p> * If {@code relativeToLength} is set to {@code true}, the actual tolerance value for a given * dimension <var>i</var> is {@code eps}×{@code length} where {@code length} is the * maximum of {@linkplain #getLength this envelope length} and the specified envelope length * along dimension <var>i</var>. * <p> * If {@code relativeToLength} is set to {@code false}, the actual tolerance value for a given * dimension <var>i</var> is {@code eps}. * <p> * Relative tolerance value (as opposed to absolute tolerance value) help to workaround the fact * that tolerance value are CRS dependent. For example the tolerance value need to be smaller * for geographic CRS than for UTM projections, because the former typically has a range of -180 * to 180° while the later can have a range of thousands of meters. * <p> * This method assumes that the specified envelope uses the same CRS than this envelope. For * performance reason, it will no be verified unless J2SE assertions are enabled. * * @param envelope * The envelope to compare with. * @param eps * The tolerance value to use for numerical comparaisons. * @param epsIsRelative * {@code true} if the tolerance value should be relative to axis length, or {@code * false} if it is an absolute value. * @return {@code true} if the given object is equals to this envelope up to the given tolerance * value. * * @see #contains(Envelope, boolean) * @see #intersects(Envelope, boolean) * * @since 2.4 */ public boolean equals(final Envelope envelope, final double eps, final boolean epsIsRelative) { ensureNonNull("envelope", envelope); final int dimension = getDimension(); if (envelope.getDimension() != dimension) { return false; } assert equalsIgnoreMetadata(crs, envelope.getCoordinateReferenceSystem()) : envelope; for (int i = 0; i < dimension; i++) { double epsilon; if (epsIsRelative) { epsilon = Math.max(getSpan(i), envelope.getSpan(i)); epsilon = (epsilon > 0 && epsilon < Double.POSITIVE_INFINITY) ? epsilon * eps : eps; } else { epsilon = eps; } // Comparaison below uses '!' in order to catch NaN values. if (!(Math.abs(getMinimum(i) - envelope.getMinimum(i)) <= epsilon && Math .abs(getMaximum(i) - envelope.getMaximum(i)) <= epsilon)) { return false; } } return true; } /** * Returns a deep copy of this envelope. * * @return A clone of this envelope. */ @Override public GeneralEnvelope clone() { try { GeneralEnvelope e = (GeneralEnvelope) super.clone(); e.ordinates = e.ordinates.clone(); return e; } catch (CloneNotSupportedException exception) { // Should not happen, since we are cloneable. throw new AssertionError(exception); } } }