blob: a023268b9028ae3e627636e62a97d7bdd264ddd3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.coverage.grid;
import java.util.Map;
import java.util.TreeMap;
import java.util.SortedMap;
import java.util.Arrays;
import java.util.Optional;
import java.util.Locale;
import java.util.logging.Logger;
import java.io.Serializable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.awt.Rectangle;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.metadata.spatial.DimensionNameType;
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.opengis.referencing.operation.TransformException;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.collection.WeakValueHashMap;
import org.apache.sis.internal.referencing.AxisDirections;
import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.geometry.AbstractEnvelope;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.TransformSeparator;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.io.TableAppender;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.LenientComparable;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.internal.system.Modules;
// Branch-dependent imports
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.GridCoordinates;
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.coverage.PointOutsideCoverageException;
/**
* A range of grid coverage coordinates, also known as "grid envelope".
* {@code GridExtent} are defined by {@linkplain #getLow() low} coordinates (often all zeros)
* and {@linkplain #getHigh() high} coordinates, <strong>inclusive</strong>.
* For example, a grid with a width of 512 cells can have a low coordinate of 0 and high coordinate of 511.
*
* <div class="note"><b>Note:</b>
* The inclusiveness of {@linkplain #getHigh() high} coordinates come from ISO 19123.
* We follow this specification for all getters methods, but developers should keep in mind
* that this is the opposite of Java2D usage where {@link Rectangle} maximal values are exclusive.</div>
*
* <p>{@code GridExtent} instances are immutable and thread-safe.
* The same instance can be shared by different {@link GridGeometry} instances.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Alexis Manin (Geomatys)
* @author Johann Sorel (Geomatys)
* @version 1.4
* @since 1.0
*/
public class GridExtent implements GridEnvelope, LenientComparable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -4717353677844056017L;
/**
* The logger for operations on grid coverages. Declared in this {@code GridExtent}
* class because it is among the first ones in the chain of dependencies.
*/
static final Logger LOGGER = Logger.getLogger(Modules.RASTER);
/**
* The dimension name types for given coordinate system axis directions.
* This map contains only the "positive" axis directions.
*
* @todo Verify if there is more directions to add as of ISO 19111:2018.
*
* @see #typeFromAxes(CoordinateReferenceSystem, int)
*/
private static final Map<AxisDirection,DimensionNameType> AXIS_DIRECTIONS = Map.of(
AxisDirection.COLUMN_POSITIVE, DimensionNameType.COLUMN,
AxisDirection.ROW_POSITIVE, DimensionNameType.ROW,
AxisDirection.UP, DimensionNameType.VERTICAL,
AxisDirection.FUTURE, DimensionNameType.TIME);
/**
* Default axis types for the two-dimensional cases.
*/
private static final DimensionNameType[] DEFAULT_TYPES = new DimensionNameType[] {
DimensionNameType.COLUMN,
DimensionNameType.ROW
};
/**
* A pool of shared {@link DimensionNameType} arrays. We use a pool
* because a small amount of arrays is shared by most grid extents.
*/
private static final WeakValueHashMap<DimensionNameType[],DimensionNameType[]> POOL = new WeakValueHashMap<>(DimensionNameType[].class);
/**
* Type of each axis (vertical, temporal, …) or {@code null} if unspecified.
* If non-null, the array length shall be equal to {@link #getDimension()}.
* Any array element may be null if unspecified for that particular axis.
* The same array may be shared by many {@code GridExtent} instances.
*
* @see #getAxisType(int)
*/
private final DimensionNameType[] types;
/**
* Minimum and maximum grid coordinates. The first half contains minimum coordinates (inclusive),
* while the last half contains maximum coordinates (<strong>inclusive</strong>). Note that the
* later inclusiveness is the opposite of Java2D usage but conforms to ISO specification.
*/
private final long[] coordinates;
/**
* Creates a new array of coordinates with the given number of dimensions.
*
* @throws IllegalArgumentException if the given number of dimensions is excessive.
*/
static long[] allocate(final int dimension) throws IllegalArgumentException {
if (dimension >= Numerics.MAXIMUM_MATRIX_SIZE) {
// Actually the real limit is Integer.MAX_VALUE / 2, but a value too high is likely to be an error.
throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension));
}
return new long[dimension << 1];
}
/**
* Checks if coordinate values in the low part are less than or
* equal to the corresponding coordinate value in the high part.
*
* @throws IllegalArgumentException if a coordinate value in the low part is
* greater than the corresponding coordinate value in the high part.
*/
private void validateCoordinates() throws IllegalArgumentException {
final int dimension = getDimension();
for (int i=0; i<dimension; i++) {
final long lower = coordinates[i];
final long upper = coordinates[i + dimension];
if (lower > upper) {
throw new IllegalArgumentException(Resources.format(
Resources.Keys.IllegalGridEnvelope_3, getAxisIdentification(i,i), lower, upper));
}
}
}
/**
* Verifies that the given array (if non-null) contains no duplicated values, then returns a copy of that array.
* The returned copy may be shared by many {@code GridExtent} instances. Consequently, it shall not be modified.
*
* @throws IllegalArgumentException if the given array contains duplicated elements.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private static DimensionNameType[] validateAxisTypes(DimensionNameType[] types) throws IllegalArgumentException {
if (types == null || ArraysExt.allEquals(types, null)) {
return null;
}
if (Arrays.equals(DEFAULT_TYPES, types)) { // Common case verified before POOL synchronized lock.
return DEFAULT_TYPES;
}
DimensionNameType[] shared = POOL.get(types);
if (shared == null) {
/*
* Verify the array only if it was not found in the pool. Arrays in the pool were already validated,
* so do not need to be verified again. The check performed here is inefficient (nested loop), but it
* should be okay since the arrays are usually small (less than 5 elements) and the checks should not
* be done often (because of the pool).
*/
types = types.clone();
for (int i=1; i<types.length; i++) {
final DimensionNameType t = types[i];
if (t != null) {
for (int j=i; --j >= 0;) {
if (t.equals(types[j])) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedElement_1, t));
}
}
}
}
shared = POOL.putIfAbsent(types, types);
if (shared == null) {
return types;
}
}
return shared;
}
/**
* Creates an initially empty grid extent with the given number of dimensions.
* All grid coordinate values are initialized to zero. This constructor is private
* because {@code GridExtent} coordinate values cannot be modified by public API.
*
* @param dimension number of dimensions.
* @param axisTypes the axis types, or {@code null} if unspecified.
*
* @see #GridExtent(GridExtent)
*/
private GridExtent(final int dimension, final DimensionNameType[] axisTypes) {
coordinates = allocate(dimension);
types = validateAxisTypes(axisTypes);
}
/**
* Creates a new grid extent for an image or matrix of the given bounds.
* The axis types are {@link DimensionNameType#COLUMN} and {@link DimensionNameType#ROW ROW} in that order.
*
* @param bounds the bounds to copy in the new grid extent.
* @throws IllegalArgumentException if the rectangle is empty.
*
* @since 1.1
*/
public GridExtent(final Rectangle bounds) {
this(bounds.width, bounds.height);
translate2D(bounds.x, bounds.y);
}
/**
* Creates a new grid extent for an image or matrix of the given size.
* The {@linkplain #getLow() low} grid coordinates are zeros and the axis types are
* {@link DimensionNameType#COLUMN} and {@link DimensionNameType#ROW ROW} in that order.
*
* @param width number of pixels in each row.
* @param height number of pixels in each column.
* @throws IllegalArgumentException if the width or the height is not greater than zero.
*/
public GridExtent(final long width, final long height) {
ArgumentChecks.ensureStrictlyPositive("width", width);
ArgumentChecks.ensureStrictlyPositive("height", height);
coordinates = new long[4];
coordinates[2] = width - 1;
coordinates[3] = height - 1;
types = DEFAULT_TYPES;
}
/**
* Creates a new grid extent for an image of the given size and location. This constructor
* is for internal usage: argument meanings differ from conventions in public constructors.
*
* @param xmin column index of the first cell.
* @param ymin row index of the first cell.
* @param width number of pixels in each row.
* @param height number of pixels in each column.
*/
GridExtent(final int xmin, final int ymin, final int width, final int height) {
this(width, height);
translate2D(xmin, ymin);
}
/**
* Completes a {@link GridExtent} construction with a final translation.
* Shall be invoked for two-dimensional extents only.
*/
private void translate2D(final long xmin, final long ymin) {
for (int i=coordinates.length; --i >= 0;) {
coordinates[i] += ((i & 1) == 0) ? xmin : ymin;
}
}
/**
* Constructs a new grid extent set to the specified coordinates.
* The given arrays contain a minimum (inclusive) and maximum value for each dimension of the grid coverage.
* The lowest valid grid coordinates are often zero, but this is not mandatory.
* As a convenience for this common case, a null {@code low} array means that all low coordinates are zero.
*
* <p>An optional (nullable) {@code axisTypes} argument can be used for attaching a label to each grid axis.
* For example if this {@code GridExtent} is four-dimensional, then the axis types may be
* {{@linkplain DimensionNameType#COLUMN column} (<var>x</var>),
* {@linkplain DimensionNameType#ROW row} (<var>y</var>),
* {@linkplain DimensionNameType#VERTICAL vertical} (<var>z</var>),
* {@linkplain DimensionNameType#TIME time} (<var>t</var>)},
* which means that the last axis is for the temporal dimension, the third axis is for the vertical dimension, <i>etc.</i>
* This information is related to the "real world" coordinate reference system axes, but not necessarily in the same order;
* it is caller responsibility to ensure that the grid axes are consistent with the CRS axes.
* The {@code axisTypes} array shall not contain duplicated elements,
* but may contain {@code null} elements if the type of some axes are unknown.</p>
*
* @param axisTypes the type of each grid axis, or {@code null} if unspecified.
* @param low the valid minimum grid coordinates (always inclusive), or {@code null} for all zeros.
* @param high the valid maximum grid coordinates, inclusive or exclusive depending on the next argument.
* @param isHighIncluded {@code true} if the {@code high} values are inclusive (as in ISO 19123 specification),
* or {@code false} if they are exclusive (as in Java2D usage).
* This argument does not apply to {@code low} values, which are always inclusive.
* @throws IllegalArgumentException if a coordinate value in the low part is
* greater than the corresponding coordinate value in the high part.
*
* @see #getLow()
* @see #getHigh()
* @see #insertDimension(int, DimensionNameType, long, long, boolean)
*/
public GridExtent(final DimensionNameType[] axisTypes, final long[] low, final long[] high, final boolean isHighIncluded) {
ArgumentChecks.ensureNonNull("high", high);
final int dimension = high.length;
if (low != null && low.length != dimension) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedDimension_2, low.length, dimension));
}
if (axisTypes != null && axisTypes.length != dimension) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedArrayLengths));
}
coordinates = allocate(dimension);
if (low != null) {
System.arraycopy(low, 0, coordinates, 0, dimension);
}
System.arraycopy(high, 0, coordinates, dimension, dimension);
if (!isHighIncluded) {
for (int i=dimension; i < coordinates.length; i++) {
coordinates[i] = Math.decrementExact(coordinates[i]);
}
}
types = validateAxisTypes(axisTypes);
validateCoordinates();
}
/**
* Infers the axis types from the given coordinate reference system.
* This method is the converse of {@link GridExtentCRS}.
*
* @param crs the coordinate reference system, or {@code null}.
* @param dimension number of name type to infer. Shall not be greater than the CRS dimension.
* @return axis types, or {@code null} if no axis were recognized.
*/
static DimensionNameType[] typeFromAxes(final CoordinateReferenceSystem crs, final int dimension) {
DimensionNameType[] axisTypes = null;
if (crs != null) {
final CoordinateSystem cs = crs.getCoordinateSystem();
for (int i=0; i<dimension; i++) {
final DimensionNameType type = AXIS_DIRECTIONS.get(AxisDirections.absolute(cs.getAxis(i).getDirection()));
if (type != null) {
if (axisTypes == null) {
axisTypes = new DimensionNameType[dimension];
}
axisTypes[i] = type;
}
}
}
return axisTypes;
}
/**
* Creates a new grid extent by rounding the given envelope to (usually) nearest integers.
* The envelope coordinates shall be cell indices with lower values inclusive and upper values exclusive.
* {@link Double#NaN} envelope coordinates will be set to the corresponding {@code enclosing} coordinates
* (an exception will be thrown if {@code enclosing} is null in that situation).
*
* Envelopes crossing the anti-meridian shall be {@linkplain GeneralEnvelope#simplify() simplified}.
* The envelope CRS is ignored, except for identifying dimension names for information purpose.
* The way floating point values are rounded to integers may be adjusted in any future version.
*
* <p><b>API note:</b> this constructor is not public because its contract is a bit approximate.</p>
*
* @param envelope the envelope containing cell indices to store in a {@code GridExtent}.
* @param rounding controls behavior of rounding from floating point values to integers.
* @param clipping how to clip this extent to the enclosing extent. Ignored if {@code enclosing} is null.
* @param margin if non-null, expands the extent by that amount of cells on each envelope dimension.
* @param chunkSize if non-null, make the grid extent spanning an integer amount of chunks (tiles).
* @param enclosing if the new grid is a sub-grid of a larger grid, that larger grid. Otherwise {@code null}.
* @param modifiedDimensions if {@code enclosing} is non-null, the grid dimensions to set from the envelope.
* The length of this array shall be equal to the {@code envelope} dimension.
* This argument is ignored if {@code enclosing} is null.
* @throws DisjointExtentException if the given envelope does not intersect the enclosing grid extent.
*
* @see #toEnvelope(MathTransform, MathTransform, Envelope)
* @see #slice(DirectPosition, int[])
*/
GridExtent(final AbstractEnvelope envelope, final GridRoundingMode rounding, final GridClippingMode clipping,
final int[] margin, final int[] chunkSize, final GridExtent enclosing, final int[] modifiedDimensions)
{
final int dimension = envelope.getDimension();
coordinates = (enclosing != null) ? enclosing.coordinates.clone() : allocate(dimension);
/*
* Assign the `types` field before we try to compute the grid extent coordinates
* because if the coordinate computation fail, `getAxisIdentification(…)` uses
* that information for producing a more informative error message if possible.
*/
if (enclosing != null && enclosing.types != null) {
types = enclosing.types;
} else {
types = validateAxisTypes(typeFromAxes(envelope.getCoordinateReferenceSystem(), dimension));
}
/*
* Now computes the grid extent coordinates.
*/
for (int i=0; i<dimension; i++) {
double min = envelope.getLower(i); // Inclusive
double max = envelope.getUpper(i); // Exclusive
final boolean isMinValid = (min >= Long.MIN_VALUE);
final boolean isMaxValid = (max <= Long.MAX_VALUE);
if (min > max || (enclosing == null && !(isMinValid & isMaxValid))) {
/*
* We do not throw an exception for NaN envelope bounds if `enclosing` is non-null
* because this case occurs when the `gridToCRS` transform has a NaN scale factor.
* Such scale factor may result from ranges like [0 … 0]. We tolerate them because
* with a non-null `enclosing` extent, we can still have grid coordinates: they are
* inherited from `enclosing`. Note that we require the two bounds to be NaN, because
* otherwise the reason for those NaN envelope bounds is not a NaN scale factor.
*/
throw new IllegalArgumentException(Resources.format(
Resources.Keys.IllegalGridEnvelope_3, getAxisIdentification(i,i), min, max));
}
if (!isMinValid) min = Long.MIN_VALUE;
if (!isMaxValid) max = Long.MAX_VALUE;
long lower, upper; // Both inclusive (upper as well).
switch (rounding) {
default: {
throw new AssertionError(rounding);
}
case ENCLOSING: {
lower = (long) Math.floor(min);
upper = (long) Math.ceil (max);
if (lower != upper) upper--; // For making the coordinate inclusive.
break;
}
case CONTAINED: {
final double lo = Math.ceil (min);
final double hi = Math.floor(max);
if (lo > hi) {
lower = (long) ((lo - min > max - hi) ? hi : lo); // Take the value closest to integer.
upper = lower;
} else {
lower = (long) lo;
upper = (long) hi;
if (lower != upper) upper--; // For making the coordinate inclusive.
}
break;
}
case NEAREST: {
lower = Math.round(min);
upper = Math.round(max);
if (lower == upper) { // Equality implies (max - min) < 1.
if (min - Math.floor(min) > Math.ceil(max) - max) {
upper = --lower;
}
} else {
upper--; // For making the coordinate inclusive.
/*
* The [lower … upper] range may be slightly larger than desired in some rounding error situations.
* For example if `min` was 1.49999 and `max` was 2.50001, the rounding will create a [1…3] range
* while there is actually only 2 pixels. We detect those rounding problems by comparing the spans
* before and after rounding. We attempt an adjustment only if the span mismatch is ±1, otherwise
* the difference is assumed to be caused by overflow. On the three values that can be affected by
* the adjustment (min, max and span), we change only the number which is farthest from an integer
* value.
*/
long delta = (upper - lower) + 1; // Negative number if overflow.
if (delta >= 0) {
final double span = envelope.getSpan(i);
final long extent = Math.round(span);
if (extent != 0 && Math.abs(delta -= extent) == 1) {
final double dmin = Math.abs(min - Math.rint(min));
final double dmax = Math.abs(max - Math.rint(max));
final boolean adjustMax = (dmax >= dmin);
if (Math.abs(span - extent) < (adjustMax ? dmax : dmin)) {
if (adjustMax) upper = Math.subtractExact(upper, delta);
else lower = Math.addExact(lower, delta);
}
}
}
}
}
}
/*
* If caller requested to clip only the user Area Of Interest (AOI) without constraining the
* margin or chunk size, then we need to do clipping now instead of at the end of this loop.
*/
if (enclosing != null && clipping == GridClippingMode.BORDER_EXPANSION) {
final int lo = (modifiedDimensions != null) ? modifiedDimensions[i] : i;
final int hi = lo + getDimension();
final long lv = Math.max(lower, coordinates[lo]);
final long hv = Math.min(upper, coordinates[hi]);
if (lv > hv) {
throw new DisjointExtentException(getAxisIdentification(lo, i),
coordinates[lo], coordinates[hi], lv, hv);
}
lower = lv;
upper = hv;
}
/*
* If the user specified a margin, add it now. The margin dimension indices follow the envelope
* dimension indices. Note that the resulting extent will be intersected with enclosing extent
* at a next step, which may cancel the margin effect.
*
* Note about overflow checks: if m>0, then x < x+m unless the result overflows the `long` capacity.
*/
if (margin != null && i < margin.length) {
final int m = margin[i];
if (enclosing != null && m > 0) {
if (lower < (lower -= m)) lower = Long.MIN_VALUE; // Clamp to MIN/MAX if overflow.
if (upper > (upper += m)) upper = Long.MAX_VALUE;
} else {
lower = Math.subtractExact(lower, m);
upper = Math.addExact(upper, m);
}
}
if (lower > upper) {
upper += (lower - upper) >>> 1; // (lower - upper)/2 as unsigned integer: overflow-safe.
lower = upper;
}
/*
* If chunk size has been specified, snap the coordinates to a multiple of that size.
* The new extent will be clipped with `enclosing` (if non-null) in next step.
* Note: formulas used here are the same than in `forChunkSize(…)` method.
*/
if (chunkSize != null && i < chunkSize.length) {
final int s = chunkSize[i];
lower = Math.subtractExact(lower, Math.floorMod(lower, s));
upper = Math.addExact(upper, (s-1) - Math.floorMod(upper, s));
}
/*
* At this point the grid range has been computed (lower to upper). Compute intersection,
* then update the coordinates accordingly. Note that if envelope coordinates were NaN,
* they will have been replaced by `Long.MIN/MAX_VALUE`, which will usually cause the
* assignation to be skipt below (so we keep the values inherited from `enclosing`).
*/
if (enclosing != null && clipping == GridClippingMode.STRICT) {
final int lo = (modifiedDimensions != null) ? modifiedDimensions[i] : i;
final int hi = lo + getDimension();
final long validMin = coordinates[lo];
final long validMax = coordinates[hi];
if (lower > validMin) coordinates[lo] = lower;
if (upper < validMax) coordinates[hi] = upper;
if (lower > validMax || upper < validMin) {
throw new DisjointExtentException(getAxisIdentification(lo, i), validMin, validMax, lower, upper);
}
} else {
coordinates[i] = lower;
coordinates[i + getDimension()] = upper;
}
}
}
/**
* Creates a new grid extent with the same axes than the given extent, but different coordinates.
* This constructor does not invoke {@link #validateCoordinates()}; we presume that the caller's
* computation is correct.
*
* @param enclosing the extent from which to copy axes, or {@code null} if none.
* @param coordinates the coordinates. This array is not cloned.
*/
GridExtent(final GridExtent enclosing, final long[] coordinates) {
this.coordinates = coordinates;
types = (enclosing != null) ? enclosing.types : null;
assert (types == null) || types.length == getDimension();
}
/**
* Creates a copy of the given grid extent. The {@link #coordinates} array is cloned
* while the {@link #types} array is shared between the two instances. This constructor
* is reserved to methods that modify the coordinates after construction. It must be
* private because we do not allow coordinates modifications by public API.
*
* @see #GridExtent(int, DimensionNameType[])
*/
private GridExtent(final GridExtent extent) {
types = extent.types;
coordinates = extent.coordinates.clone();
}
/**
* Creates a new grid extent as a copy of the given one.
*
* @param extent the grid extent to copy.
* @throws IllegalArgumentException if a coordinate value in the low part is
* greater than the corresponding coordinate value in the high part.
*
* @see #castOrCopy(GridEnvelope)
*/
protected GridExtent(final GridEnvelope extent) {
ArgumentChecks.ensureNonNull("extent", extent);
final int dimension = extent.getDimension();
coordinates = allocate(dimension);
for (int i=0; i<dimension; i++) {
coordinates[i] = extent.getLow(i);
coordinates[i + dimension] = extent.getHigh(i);
}
types = (extent instanceof GridExtent) ? ((GridExtent) extent).types : null;
validateCoordinates();
}
/**
* Returns the given grid extent as a {@code GridExtent} implementation.
* If the given extent is already a {@code GridExtent} instance or is null, then it is returned as-is.
* Otherwise a new extent is created using the {@linkplain #GridExtent(GridEnvelope) copy constructor}.
*
* @param extent the grid extent to cast or copy, or {@code null}.
* @return the grid extent as a {@code GridExtent}, or {@code null} if the given extent was null.
*/
public static GridExtent castOrCopy(final GridEnvelope extent) {
if (extent == null || extent instanceof GridExtent) {
return (GridExtent) extent;
} else {
return new GridExtent(extent);
}
}
/**
* Returns the number of dimensions.
*
* @return the number of dimensions.
*
* @see #selectDimensions(int[])
*/
@Override
public final int getDimension() {
return coordinates.length >>> 1;
}
/**
* Returns the number of dimensions where this grid extent has a size greater than 1.
* This is a value between 0 and {@link #getDimension()} inclusive.
*
* @return the number of dimensions where this grid extent has a size greater than 1.
*
* @see #getSubspaceDimensions(int)
*/
final int getSubDimension() {
int n = 0;
final int dimension = getDimension();
for (int i=0; i<dimension; i++) {
if (coordinates[i] != coordinates[i + dimension]) n++;
}
return n;
}
/**
* Returns {@code true} if all low coordinates are zero.
* This is a very common case since many grids start their cell numbering at zero.
*
* @return whether all low coordinates are zero.
*
* @see #translate(long...)
*/
public boolean startsAtZero() {
return isZero(coordinates, getDimension());
}
/**
* Returns {@code true} if all values in the given vector are zero.
*
* @param vector the vector to verify.
* @param n number of elements to verify. All remaining elements are ignored.
* @return whether the <var>n</var> first elements in the given array are all zero.
*/
private static boolean isZero(final long[] vector, int n) {
while (--n >= 0) {
if (vector[n] != 0) {
return false;
}
}
return true;
}
/**
* Returns the valid minimum grid coordinates, inclusive.
* The sequence contains a minimum value for each dimension of the grid coverage.
*
* @return the valid minimum grid coordinates, inclusive.
*
* @see #getLow(int)
*/
@Override
public GridCoordinates getLow() {
return new GridCoordinatesView(coordinates, 0);
}
/**
* Returns the valid maximum grid coordinates, <strong>inclusive</strong>.
* The sequence contains a maximum value for each dimension of the grid coverage.
*
* @return the valid maximum grid coordinates, <strong>inclusive</strong>.
*
* @see #getHigh(int)
*/
@Override
public GridCoordinates getHigh() {
return new GridCoordinatesView(coordinates, getDimension());
}
/**
* Returns the valid minimum inclusive grid coordinate along the specified dimension.
*
* @param index the dimension for which to obtain the coordinate value.
* @return the low coordinate value at the given dimension, inclusive.
* @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
* than the {@linkplain #getDimension() grid dimension}.
*
* @see #getLow()
* @see #getHigh(int)
* @see #withRange(int, long, long)
*/
@Override
public long getLow(final int index) {
ArgumentChecks.ensureValidIndex(getDimension(), index);
return coordinates[index];
}
/**
* Returns the valid maximum <strong>inclusive</strong> grid coordinate along the specified dimension.
*
* @param index the dimension for which to obtain the coordinate value.
* @return the high coordinate value at the given dimension, <strong>inclusive</strong>.
* @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
* than the {@linkplain #getDimension() grid dimension}.
*
* @see #getHigh()
* @see #getLow(int)
* @see #withRange(int, long, long)
*/
@Override
public long getHigh(final int index) {
final int dimension = getDimension();
ArgumentChecks.ensureValidIndex(dimension, index);
return coordinates[index + dimension];
}
/**
* Returns the average of low and high coordinates, rounded toward positive infinity.
* This method is equivalent to computing any of the following,
* except that this method does not overflow even if the sum would overflow:
*
* <ul>
* <li>(<var>low</var> + <var>high</var>) / 2 rounded toward positive infinity, or</li>
* <li>(<var>low</var> + <var>high</var> + 1) / 2 rounded toward negative infinity.</li>
* </ul>
*
* The two above formulas are equivalent, so the result does not depend
* on whether the high coordinate should be inclusive or exclusive.
*
* @param index the dimension for which to obtain the coordinate value.
* @return the median coordinate value at the given dimension.
* @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
* than the {@linkplain #getDimension() grid dimension}.
*
* @since 1.3
*/
public long getMedian(final int index) {
final int dimension = getDimension();
ArgumentChecks.ensureValidIndex(dimension, index);
final long low = coordinates[index];
final long high = coordinates[index + dimension];
/*
* Use `>> 1` instead of `/2` because the two operations differ in their rounding mode for negative values.
* The former rounds toward negative infinity (which is intended here) while the latter rounds toward zero.
* If at least one value is odd, add +1 to the result.
*/
return (low >> 1) + (high >> 1) + ((low | high) & 1);
}
/**
* Returns the number of integer grid coordinates along the specified dimension.
* This is equal to {@code getHigh(dimension) - getLow(dimension) + 1}.
*
* @param index the dimension for which to obtain the size.
* @return the number of integer grid coordinates along the given dimension.
* @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
* than the {@linkplain #getDimension() grid dimension}.
* @throws ArithmeticException if the size is too large for the {@code long} primitive type.
*
* @see #getLow(int)
* @see #getHigh(int)
* @see #resize(long...)
*/
@Override
public long getSize(final int index) {
final int dimension = getDimension();
ArgumentChecks.ensureValidIndex(dimension, index);
return Math.incrementExact(Math.subtractExact(coordinates[dimension + index], coordinates[index]));
}
/**
* Returns the number of grid coordinates as a double precision floating point value.
* Invoking this method is equivalent to invoking {@link #getSize(int)} and converting
* the result from {@code long} to the {@code double} primitive type, except that this
* method does not overflow (i.e. does not throw {@link ArithmeticException}).
*
* @param index the dimension for which to obtain the size.
* @param minusOne {@code true} for returning <var>size</var>−1 instead of <var>size</var>.
* @return the number of integer grid coordinates along the given dimension.
*/
public double getSize(final int index, final boolean minusOne) {
final int dimension = getDimension();
ArgumentChecks.ensureValidIndex(dimension, index);
long size = coordinates[dimension + index] - coordinates[index]; // Unsigned long.
if (!minusOne && ++size == 0) {
return 0x1P64; // Unsigned integer overflow. Result is 2^64.
}
return Numerics.toUnsignedDouble(size);
}
/**
* Returns the grid coordinates of a representative point.
* This point may be used for estimating a {@linkplain GridGeometry#getResolution(boolean) grid resolution}.
* The default implementation returns the median (or center) coordinates of this grid extent,
* but subclasses can override this method if another point is considered more representative.
*
* <p>The {@code anchor} argument tells {@linkplain GridGeometry#getGridToCRS(PixelInCell) which transform}
* the caller intend to use for converting the grid coordinates to "real world" coordinates.
* With the default implementation, the coordinate values returned with {@code CELL_CORNER}
* are 0.5 cell units higher than the coordinate values returned with {@code CELL_CENTER}.
* Subclasses are free to ignore this argument.</p>
*
* @param anchor the convention to be used for conversion to "real world" coordinates.
* @return the grid coordinates of a representative point.
*
* @since 1.3
*/
public double[] getPointOfInterest(final PixelInCell anchor) {
final int dimension = getDimension();
final double[] center = new double[dimension];
final boolean isCorner = PixelInCell.CELL_CORNER.equals(anchor);
for (int i=0; i<dimension; i++) {
/*
* We want the average of (low + hi+1). However for the purpose of computing an average, it does
* not matter if we add 1 to `low` or `hi`. So we add 1 to `low` because it should not overflow.
*/
long low = coordinates[i];
if (isCorner) {
low = Math.incrementExact(coordinates[i]);
}
center[i] = MathFunctions.average(low, coordinates[i + dimension]);
}
return center;
}
/**
* Returns the grid coordinates for all dimensions where the grid has a size of 1.
* Keys are dimensions as values from 0 inclusive to {@link #getDimension()} exclusive.
* Values are the {@linkplain #getLow(int) low} and {@linkplain #getHigh(int) high} coordinates
* (which are equal) in the associated dimension.
*
* @return grid coordinates for all dimensions where the grid has a size of 1.
*
* @see GridCoverage.Evaluator#setDefaultSlice(Map)
*
* @since 1.3
*/
public SortedMap<Integer,Long> getSliceCoordinates() {
final TreeMap<Integer,Long> slice = new TreeMap<>();
final int dimension = getDimension();
for (int i=0; i<dimension; i++) {
final long value = coordinates[i];
if (value == coordinates[i + dimension]) {
slice.put(i, value);
}
}
return slice;
}
/**
* Returns indices of all dimensions where this grid extent has a size greater than 1.
* This method can be used for getting the grid extent of a <var>s</var>-dimensional slice
* in a <var>n</var>-dimensional cube where <var>s</var> ≤ <var>n</var>.
*
* <h4>Example</h4>
* suppose that we want to get a two-dimensional slice <var>(y,z)</var> in a four-dimensional data cube <var>(x,y,z,t)</var>.
* The first step is to specify the <var>x</var> and <var>t</var> coordinates of the slice.
* In this example we set <var>x</var> to 5 and <var>t</var> to 8.
*
* {@snippet lang="java" :
* GridGeometry grid = ...; // Geometry of the (x,y,z,t) grid.
* GridGeometry slice4D = grid.slice(new GeneralDirectPosition(5, NaN, NaN, 8));
* }
*
* Above code created a slice at the requested position, but that slice still have 4 dimensions.
* It is a "slice" because the <var>x</var> and <var>t</var> dimensions of {@code slice4D} have only one cell.
* If a two-dimensional slice is desired, then above operations can be completed as below.
* In this example, the result of {@code getSubspaceDimensions(2)} call will be {1,2}.
*
* {@snippet lang="java" :
* int[] subDimensions = slice4D.getExtent().getSubspaceDimensions(2);
* GridGeometry slice2D = slice4D.reduce(subDimensions);
* }
*
* Note that in this particular example, it would have been more efficient to execute {@code grid.reduce(1,2)} directly.
* This {@code getSubspaceDimensions(int)} method is more useful for inferring a {@code slice2D} from a {@code slice4D}
* which has been created elsewhere, or when we do not really want the {@code slice2D} but only its dimension indices.
*
* <h4>Number of dimensions</h4>
* This method returns exactly <var>s</var> indices. If there is more than <var>s</var> dimensions having a
* {@linkplain #getSize(int) size} greater than 1, then a {@link SubspaceNotSpecifiedException} is thrown.
* If there is less than <var>s</var> dimensions having a size greater than 1, then the returned list of
* dimensions is completed with some dimensions of size 1, starting with the first dimensions in this grid
* extent, until there is exactly <var>s</var> dimensions. If this grid extent does not have at least
* <var>s</var> dimensions, then a {@link CannotEvaluateException} is thrown.
*
* @param s number of dimensions of the sub-space.
* @return indices of sub-space dimensions, in increasing order in an array of length <var>s</var>.
* @throws SubspaceNotSpecifiedException if there is more than <var>s</var> dimensions having a size greater than 1.
* @throws CannotEvaluateException if this grid extent does not have at least <var>s</var> dimensions.
*/
public int[] getSubspaceDimensions(final int s) {
ArgumentChecks.ensurePositive("s", s);
final int m = getDimension();
if (s > m) {
throw new CannotEvaluateException(Resources.format(Resources.Keys.GridEnvelopeMustBeNDimensional_1, s));
}
final int[] selected = new int[s];
int count = 0;
for (int i=0; i<m; i++) {
final long low = coordinates[i];
final long high = coordinates[i+m];
if (low != high) {
if (count < s) {
selected[count++] = i;
} else {
long size = high - low;
if (size != -1) size++; // When interpreted as unsigned long, -1 is the maximal value.
throw new SubspaceNotSpecifiedException(Resources.format(Resources.Keys.NoNDimensionalSlice_3,
s, getAxisIdentification(i,i), Numerics.toUnsignedDouble(size)));
}
}
}
if (s != count) {
for (int i=0; ; i++) {
// An IndexOutOfBoundsException would be a bug in our algorithm.
if (coordinates[i] == coordinates[i+m]) {
selected[count++] = i;
if (count == s) break;
}
}
Arrays.sort(selected);
}
return selected;
}
/**
* Returns the type (vertical, temporal, …) of grid axis at given dimension.
* This information is provided because the grid axis type cannot always be inferred from the context.
* Some examples are:
*
* <ul>
* <li>{@code getAxisType(0)} may return {@link DimensionNameType#COLUMN},
* {@link DimensionNameType#TRACK TRACK} or {@link DimensionNameType#LINE LINE}.</li>
* <li>{@code getAxisType(1)} may return {@link DimensionNameType#ROW},
* {@link DimensionNameType#CROSS_TRACK CROSS_TRACK} or {@link DimensionNameType#SAMPLE SAMPLE}.</li>
* <li>{@code getAxisType(2)} may return {@link DimensionNameType#VERTICAL}.</li>
* <li>{@code getAxisType(3)} may return {@link DimensionNameType#TIME}.</li>
* </ul>
*
* Above are only examples; there are no constraints on axis order. In particular grid axes do not need to be in the same
* order than the corresponding {@linkplain GridGeometry#getCoordinateReferenceSystem() coordinate reference system} axes.
*
* @param index the dimension for which to obtain the axis type.
* @return the axis type at the given dimension. May be absent if the type is unknown.
* @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
* than the {@linkplain #getDimension() grid dimension}.
*/
public Optional<DimensionNameType> getAxisType(final int index) {
ArgumentChecks.ensureValidIndex(getDimension(), index);
return Optional.ofNullable((types != null) ? types[index] : null);
}
/**
* Returns the {@link #types} array or a default array of arbitrary length if {@link #types} is null.
* This method returns directly the arrays without cloning; do not modify.
*/
final DimensionNameType[] getAxisTypes() {
return (types != null) ? types : DEFAULT_TYPES;
}
/**
* Returns the axis number followed by the localized axis type if available.
* This is used for error messages only.
*
* @param index index of the dimension as stored in this grid extent.
* @param indexShown index to write in the message. Often the same as {@code index}.
*/
final Object getAxisIdentification(final int index, final int indexShown) {
if (types != null) {
final DimensionNameType type = types[index];
if (type != null) {
return indexShown + " (" + Types.getCodeTitle(type) + ')';
}
}
return indexShown;
}
/**
* Returns a grid extent identical to this grid extent except for the coordinate values in the specified dimension.
* This grid extent is not modified.
*
* @param index the dimension for which to set the coordinate values.
* @param low the low coordinate value at the given dimension, inclusive.
* @param high the high coordinate value at the given dimension, <strong>inclusive</strong>.
* @return a grid extent with the specified coordinate values, or {@code this} if values are unchanged.
* @throws IllegalArgumentException if the low coordinate value is greater than the high coordinate value.
*
* @see #getLow(int)
* @see #getHigh(int)
*
* @since 1.3
*/
public GridExtent withRange(final int index, final long low, final long high) {
int ih = getDimension();
ArgumentChecks.ensureValidIndex(ih, index);
ih += index;
if (coordinates[index] == low && coordinates[ih] == high) {
return this;
}
if (low > high) {
throw new IllegalArgumentException(Resources.format(
Resources.Keys.IllegalGridEnvelope_3, getAxisIdentification(index, index), low, high));
}
final GridExtent copy = new GridExtent(this);
copy.coordinates[index] = low;
copy.coordinates[ih] = high;
return copy;
}
/**
* Transforms this grid extent to a "real world" envelope using the given transform.
* The transform shall map <em>cell corner</em> to real world coordinates.
*
* @param cornerToCRS a transform from <em>cell corners</em> to real world coordinates.
* @return this grid extent in real world coordinates.
* @throws TransformException if the envelope cannot be computed with the given transform.
*
* @see GridGeometry#getEnvelope()
* @see org.opengis.referencing.datum.PixelInCell#CELL_CORNER
*
* @since 1.1
*/
public GeneralEnvelope toEnvelope(final MathTransform cornerToCRS) throws TransformException {
ArgumentChecks.ensureNonNull("cornerToCRS", cornerToCRS);
final GeneralEnvelope envelope = toEnvelope(cornerToCRS, cornerToCRS, null);
final Matrix gridToCRS = MathTransforms.getMatrix(cornerToCRS);
if (gridToCRS != null && Matrices.isAffine(gridToCRS)) try {
envelope.setCoordinateReferenceSystem(GridExtentCRS.forExtentAlone(gridToCRS, getAxisTypes()));
} catch (FactoryException e) {
throw new TransformException(e);
}
return envelope;
}
/**
* Transforms this grid extent to a "real world" envelope using the given transform.
* The transform shall map <em>cell corner</em> to real world coordinates.
* This method does not set the envelope coordinate reference system.
*
* @param cornerToCRS a transform from <em>cell corners</em> to real world coordinates.
* @param gridToCRS the transform specified by the user. May be the same as {@code cornerToCRS}.
* If different, then this is assumed to map cell centers instead of cell corners.
* @param fallback bounds to use if some values are still NaN after conversion, or {@code null} if none.
* @return this grid extent in real world coordinates.
* @throws TransformException if the envelope cannot be computed with the given transform.
*
* @see #GridExtent(AbstractEnvelope, GridRoundingMode, int[], GridExtent, int[])
*
* @see GridGeometry#getEnvelope(CoordinateReferenceSystem)
*/
final GeneralEnvelope toEnvelope(final MathTransform cornerToCRS, final MathTransform gridToCRS, final Envelope fallback)
throws TransformException
{
final GeneralEnvelope envelope = Envelopes.transform(cornerToCRS, toEnvelope());
complete(envelope, gridToCRS, gridToCRS != cornerToCRS, fallback);
return envelope;
}
/**
* Returns the coordinates of this grid extent in an envelope.
* The returned envelope has no CRS.
*/
final GeneralEnvelope toEnvelope() {
final int dimension = getDimension();
final GeneralEnvelope envelope = new GeneralEnvelope(dimension);
for (int i=0; i<dimension; i++) {
long high = coordinates[i + dimension];
if (high != Long.MAX_VALUE) high++; // Make the coordinate exclusive before cast.
envelope.setRange(i, coordinates[i], high); // Possible loss of precision in cast to `double` type.
}
return envelope;
}
/**
* Transforms this grid extent to "real world" envelopes using the given transform.
* This method usually returns exactly one envelope, but may return more envelopes if the given transform
* contains at least one {@link org.apache.sis.referencing.operation.transform.WraparoundTransform} step.
*
* @param cornerToCRS a transform from <em>cell corners</em> to real world coordinates.
* @param gridToCRS the transform specified by the user. May be the same as {@code cornerToCRS}.
* If different, then this is assumed to map cell centers instead of cell corners.
* @param fallback bounds to use if some values are still NaN after conversion, or {@code null} if none.
* @return this grid extent in real world coordinates.
* @throws TransformException if the envelope cannot be computed with the given transform.
*
* @see #GridExtent(AbstractEnvelope, GridRoundingMode, int[], GridExtent, int[])
*
* @see GridGeometry#getEnvelope(CoordinateReferenceSystem)
*/
final GeneralEnvelope[] toEnvelopes(final MathTransform cornerToCRS, final MathTransform gridToCRS, final Envelope fallback)
throws TransformException
{
final GeneralEnvelope[] envelopes = Envelopes.wraparound(cornerToCRS, toEnvelope());
for (final GeneralEnvelope envelope : envelopes) {
complete(envelope, gridToCRS, gridToCRS != cornerToCRS, fallback);
}
return envelopes;
}
/**
* If the envelope contains some NaN values, tries to replace them by constant values inferred from the math transform.
* We must use the {@link MathTransform} specified by the user ({@code gridToCRS}), not necessarily {@code cornerToCRS},
* because inferring a {@code cornerToCRS} by translating a {@code centerToCRS} by 0.5 cell increase the amount of NaN
* values in the matrix. For giving a chance to {@link TransformSeparator} to perform its work,
* we need the minimal amount of NaN values.
*
* @param envelope the envelope to complete if empty.
* @param gridToCRS the transform specified by user.
* @param isCenter whether the "grid to CRS" transform maps cell center instead of cell corners.
* @param fallback bounds to use if some values are still NaN after conversion, or {@code null} if none.
*/
private void complete(final GeneralEnvelope envelope, final MathTransform gridToCRS, final boolean isCenter, final Envelope fallback) {
if (envelope.isEmpty()) try {
final int dimension = getDimension();
TransformSeparator separator = null;
for (int srcDim=0; srcDim < dimension; srcDim++) {
if (coordinates[srcDim + dimension] == 0 && coordinates[srcDim] == 0) {
/*
* At this point we found a grid dimension with [0 … 0] range. Only this specific range is processed because
* it is assumed associated to NaN scale factors in the `gridToCRS` matrix, since the resolution is computed
* by 0/0. We require the range to be [0 … 0] instead of [n … n] because if grid indices are not zero, then
* we would need to know the scale factors for computing the offset.
*/
if (separator == null) {
separator = new TransformSeparator(gridToCRS);
}
separator.addSourceDimensionRange(srcDim, srcDim + 1);
final Matrix component = MathTransforms.getMatrix(separator.separate());
if (component != null) {
final int[] targets = separator.getTargetDimensions();
for (int j=0; j<targets.length; j++) {
final int tgtDim = targets[j];
double lower = envelope.getLower(tgtDim);
double upper = envelope.getUpper(tgtDim);
final double value = component.getElement(j, component.getNumCol() - 1);
/*
* Replace only the envelope NaN values by the translation term (non-NaN values are left unchanged).
* If the gridToCRS map cell corners, then we update only the lower bound since the transform maps
* lower-left corner; the upper bound is unknown. If the gridToCRS maps cell center, then we update
* both lower and upper bounds to a value assumed to be in the center; the span is set to zero.
*/
if (isCenter) {
double span = upper - value;
if (Double.isNaN(span)) {
span = value - lower;
if (Double.isNaN(span)) {
span = 0;
}
}
if (Double.isNaN(lower)) lower = value - span;
if (Double.isNaN(upper)) upper = value + span;
} else if (Double.isNaN(lower)) {
lower = value;
}
envelope.setRange(tgtDim, lower, upper);
}
}
separator.clear();
}
}
/*
* If above block has been unable to fix all NaN values, fix the remaining NaNs by copying the corresponding
* coordinates from the fallback envelope. It should happen only for dimensions with a thickness of 1, i.e.
* when `low == high` but not necessarily `low == 0` and `high == 0` (contrarily to above block). We use this
* fallback is last resort because the envelope may be less reliable than values computed from `gridToCRS`.
*/
if (fallback != null) {
for (int tgtDim = envelope.getDimension(); --tgtDim >= 0;) {
boolean modified = false;
double lower = envelope.getLower(tgtDim);
double upper = envelope.getUpper(tgtDim);
if (Double.isNaN(lower)) {lower = fallback.getMinimum(tgtDim); modified = true;}
if (Double.isNaN(upper)) {upper = fallback.getMaximum(tgtDim); modified = true;}
if (modified && !(lower > upper)) { // Use `!` for accepting NaN.
envelope.setRange(tgtDim, lower, upper);
}
}
}
} catch (FactoryException e) {
// "toEnvelope" is the closest public method that may invoke this method.
Logging.recoverableException(LOGGER, GridExtent.class, "toEnvelope", e);
}
}
/**
* Returns a new grid extent with the specified dimension inserted at the given index in this grid extent.
* To append a new dimension after all existing dimensions, set {@code offset} to {@link #getDimension()}.
*
* @param offset where to insert the new dimension, from 0 to {@link #getDimension()} inclusive.
* @param axisType the type of the grid axis to add, or {@code null} if unspecified.
* @param low the valid minimum grid coordinate (always inclusive).
* @param high the valid maximum grid coordinate, inclusive or exclusive depending on the next argument.
* @param isHighIncluded {@code true} if the {@code high} value is inclusive (as in ISO 19123 specification),
* or {@code false} if it is exclusive (as in Java2D usage).
* This argument does not apply to {@code low} value, which is always inclusive.
* @return a new grid extent with the specified dimension added.
* @throws IllegalArgumentException if the low coordinate value is greater than the high coordinate value.
*
* @see #selectDimensions(int...)
*
* @since 1.1
*/
public GridExtent insertDimension(final int offset, final DimensionNameType axisType, final long low, long high, final boolean isHighIncluded) {
final int dimension = getDimension();
ArgumentChecks.ensureBetween("offset", 0, dimension, offset);
if (!isHighIncluded) {
high = Math.decrementExact(high);
}
final int newDim = dimension + 1;
DimensionNameType[] axisTypes = null;
if (types != null || axisType != null) {
if (types != null) {
axisTypes = ArraysExt.insert(types, offset, 1);
} else {
axisTypes = new DimensionNameType[newDim];
}
axisTypes[offset] = axisType;
}
final GridExtent ex = new GridExtent(newDim, axisTypes);
System.arraycopy(coordinates, 0, ex.coordinates, 0, offset);
System.arraycopy(coordinates, offset, ex.coordinates, offset + 1, dimension - offset);
System.arraycopy(coordinates, dimension, ex.coordinates, newDim, offset);
System.arraycopy(coordinates, dimension + offset, ex.coordinates, newDim + offset + 1, dimension - offset);
ex.coordinates[offset] = low;
ex.coordinates[offset + newDim] = high;
ex.validateCoordinates();
return ex;
}
/**
* Returns a grid extent that encompass only some dimensions of this grid extent.
* This method copies the specified dimensions of this grid extent into a new grid extent.
* The given dimensions must be in strictly ascending order without duplicated values.
* The number of dimensions of the sub grid extent will be {@code dimensions.length}.
*
* <p>This method performs a <cite>dimensionality reduction</cite> and can be used as the
* converse of {@link #insertDimension(int, DimensionNameType, long, long, boolean)}.
* This method cannot be used for changing dimension order.</p>
*
* @param dimensions the dimensions to select, in strictly increasing order.
* @return the sub-envelope, or {@code this} if the given array contains all dimensions of this grid extent.
* @throws IndexOutOfBoundsException if an index is out of bounds.
*
* @see #getSubspaceDimensions(int)
* @see GridGeometry#selectDimensions(int...)
*
* @since 1.3
*/
public GridExtent selectDimensions(int... dimensions) {
dimensions = verifyDimensions(dimensions, getDimension());
return (dimensions != null) ? reorder(dimensions) : this;
}
/**
* Verifies the validity of a given {@code dimensions} argument.
*
* @param dimensions the user supplied argument to validate.
* @param limit maximal number of dimensions, exclusive.
* @return a clone of the given array, or {@code null} if the caller can return {@code this}.
*/
static int[] verifyDimensions(int[] dimensions, final int limit) {
ArgumentChecks.ensureNonNull("dimensions", dimensions);
final int n = dimensions.length;
ArgumentChecks.ensureSizeBetween("dimensions", 1, limit, n);
dimensions = dimensions.clone();
if (!ArraysExt.isSorted(dimensions, true)) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.NotStrictlyOrderedDimensions));
}
int d = dimensions[0];
if (d >= 0) {
d = dimensions[n - 1];
if (d < limit) {
return (n != limit) ? dimensions : null;
}
}
throw new IndexOutOfBoundsException(Errors.format(Errors.Keys.IndexOutOfBounds_1, d));
}
/**
* Changes axis order or reduces the number of dimensions.
* It is caller responsibility to ensure that the given dimensions are valid.
*/
final GridExtent reorder(final int[] dimensions) {
final int sd = getDimension();
final int td = dimensions.length;
DimensionNameType[] tt = null;
if (types != null) {
tt = new DimensionNameType[td];
for (int i=0; i<td; i++) {
tt[i] = types[dimensions[i]];
}
}
final GridExtent sub = new GridExtent(td, tt);
for (int i=0; i<td; i++) {
final int j = dimensions[i];
sub.coordinates[i] = coordinates[j];
sub.coordinates[i+td] = coordinates[j+sd];
}
return sub;
}
/**
* Returns a grid extent expanded by the given amount of cells on both sides along each dimension.
* This method adds the given margins to the {@linkplain #getHigh(int) high coordinates}
* and subtracts the same margins from the {@linkplain #getLow(int) low coordinates}.
* If a negative margin is supplied, the extent size decreases accordingly.
*
* <h4>Number of arguments</h4>
* The {@code margins} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, missing values default to 0 (i.e. sizes in unspecified dimensions are unchanged).
* If the array is longer, extraneous values are ignored.
*
* @param margins amount of cells to add or subtract on both sides for each dimension.
* @return a grid extent expanded by the given amount, or {@code this} if there is no change.
* @throws ArithmeticException if expanding this extent by the given margins overflows {@code long} capacity.
*
* @see GridDerivation#margin(int...)
*/
public GridExtent expand(final long... margins) {
ArgumentChecks.ensureNonNull("margins", margins);
final int m = getDimension();
final int length = Math.min(m, margins.length);
if (isZero(margins, length)) {
return this;
}
final GridExtent resized = new GridExtent(this);
final long[] c = resized.coordinates;
for (int i=0; i<length; i++) {
final long p = margins[i];
c[i] = Math.subtractExact(c[i], p);
c[i+m] = Math.addExact(c[i+m], p);
}
return resized;
}
/**
* Returns a grid extent expanded by the minimal amount of cells needed for covering an integer amount of chunks.
* The grid coordinates (0, 0, …) locate the corner of a chunk.
*
* <h4>Number of arguments</h4>
* The {@code sizes} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, missing values default to 1. If the array is longer, extraneous values are ignored.
*
* @param sizes number of cells in all tiles or chunks.
* @return a grid extent expanded for the given chunk size.
*
* @see GridDerivation#chunkSize(int...)
*/
final GridExtent forChunkSize(final int... sizes) {
/*
* Current implementation does not validate argument because this method is not public.
* If we make this method public in the future, argument validation should be added.
*/
final int m = getDimension();
final int length = Math.min(m, sizes.length);
final GridExtent resized = new GridExtent(this);
final long[] c = resized.coordinates;
for (int i=0; i<length; i++) {
final int s = sizes[i];
final int j = i + m;
c[i] = Math.subtractExact(c[i], Math.floorMod(c[i], s));
c[j] = Math.addExact(c[j], (s-1) - Math.floorMod(c[j], s));
}
return resized;
}
/**
* Sets the size of grid extent to the given values by moving low and high coordinates.
* This method modifies grid coordinates as if they were multiplied by
*
* <var>(given size)</var> / <var>({@linkplain #getSize(int) current size})</var>,
*
* rounded toward zero and with the value farthest from zero adjusted by ±1 for having a size
* exactly equals to the specified value.
*
* In the common case where the {@linkplain #getLow(int) low value} is zero,
* this is equivalent to setting the {@linkplain #getHigh(int) high value} to {@code size} - 1.
*
* <h4>Number of arguments</h4>
* The {@code sizes} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, sizes in unspecified dimensions are unchanged.
* If the array is longer, extraneous values are ignored.
*
* @param sizes the new grid sizes for each dimension.
* @return a grid extent having the given sizes, or {@code this} if there is no change.
* @throws ArithmeticException if resizing this extent to the given size overflows {@code long} capacity.
*
* @see #getSize(int)
* @see GridDerivation#subgrid(GridExtent, int...)
*/
public GridExtent resize(final long... sizes) {
ArgumentChecks.ensureNonNull("sizes", sizes);
final int m = getDimension();
final int length = Math.min(m, sizes.length);
final GridExtent resize = new GridExtent(this);
final long[] c = resize.coordinates;
for (int i=0; i<length; i++) {
final long size = sizes[i];
if (size <= 0) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.ValueNotGreaterThanZero_2, Strings.toIndexed("sizes", i), size));
}
long lower = c[i];
long upper = c[i+m];
final long current = Math.incrementExact(Math.subtractExact(upper, lower));
if (Math.abs(lower) <= Math.abs(upper)) {
lower = Numerics.multiplyDivide(lower, size, current);
upper = Math.addExact(lower, size - 1);
} else {
upper = Numerics.multiplyDivide(upper, size, current);
lower = Math.subtractExact(upper, size - 1);
}
c[i ] = lower;
c[i+m] = upper;
}
return Arrays.equals(c, coordinates) ? this : resize;
}
/**
* Creates a new grid extent subsampled by the given amount of cells along each grid dimensions.
* This method divides {@linkplain #getLow(int) low coordinates} and {@linkplain #getSize(int) grid sizes}
* by the given periods, rounding toward zero. The {@linkplain #getHigh(int) high coordinates} are adjusted
* accordingly (this is often equivalent to dividing high coordinates by the periods too, but a difference
* of one cell may exist).
*
* <div class="note"><b>Note:</b>
* If the "real world" envelope computed from grid extent needs to stay approximately the same, then the
* {@linkplain GridGeometry#getGridToCRS grid to CRS} transform needs to compensate the subsampling with
* a pre-multiplication of each grid coordinates by {@code periods}.
* However, the envelope computed that way may become <em>larger</em> after subsampling, not smaller.
* This effect can be understood intuitively if we consider that cells become larger after subsampling,
* which implies that accurate representation of the same envelope may require fractional cells on some
* grid borders.</div>
*
* This method does not reduce the number of dimensions of the grid extent.
* For dimensionality reduction, see {@link #selectDimensions(int[])}.
*
* <h4>Number of arguments</h4>
* The {@code periods} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, missing values default to 1 (i.e. samplings in unspecified dimensions are unchanged).
* If the array is longer, extraneous values are ignored.
*
* @param periods the subsampling. Length shall be equal to the number of dimension and all values shall be greater than zero.
* @return the subsampled extent, or {@code this} if subsampling results in the same extent.
* @throws IllegalArgumentException if a period is not greater than zero.
*
* @see GridDerivation#subgrid(GridExtent, int...)
*/
public GridExtent subsample(final int... periods) {
ArgumentChecks.ensureNonNull("periods", periods);
final int m = getDimension();
final int length = Math.min(m, periods.length);
final GridExtent sub = new GridExtent(this);
for (int i=0; i<length; i++) {
final int s = periods[i];
if (s > 1) {
final int j = i + m;
long low = coordinates[i];
long size = coordinates[j] - low + 1; // Result is an unsigned number.
if (size == 0) {
throw new ArithmeticException(Errors.format(Errors.Keys.IntegerOverflow_1, Long.SIZE));
}
long r = Long.divideUnsigned(size, s);
if (r*s == size) r--; // Make inclusive if the division did not already rounded toward 0.
sub.coordinates[i] = low /= s;
sub.coordinates[j] = low + r;
} else if (s <= 0) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.ValueNotGreaterThanZero_2, Strings.toIndexed("periods", i), s));
}
}
return Arrays.equals(coordinates, sub.coordinates) ? this : sub;
}
/**
* Creates a new grid extent upsampled by the given amount of cells along each grid dimensions.
* This method multiplies {@linkplain #getLow(int) low} and {@linkplain #getHigh(int) high} coordinates
* by the given periods.
*
* This method does not change the number of dimensions of the grid extent.
*
* <h4>Number of arguments</h4>
* The {@code periods} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, missing values default to 1 (i.e. samplings in unspecified dimensions are unchanged).
* If the array is longer, extraneous values are ignored.
*
* @param periods the upsampling. Length shall be equal to the number of dimension and all values shall be greater than zero.
* @return the upsampled extent, or {@code this} if upsampling results in the same extent.
* @throws IllegalArgumentException if a period is not greater than zero.
* @throws ArithmeticException if the upsampled extent overflows the {@code long} capacity.
*
* @see GridGeometry#upsample(int...)
* @since 1.3
*/
public GridExtent upsample(final int... periods) {
ArgumentChecks.ensureNonNull("periods", periods);
final int m = getDimension();
final int length = Math.min(m, periods.length);
final GridExtent sub = new GridExtent(this);
for (int i=0; i<length; i++) {
final int s = periods[i];
if (s > 1) {
final int j = i + m;
sub.coordinates[i] = Math.multiplyExact(coordinates[i], s);
sub.coordinates[j] = Math.addExact(Math.multiplyExact(coordinates[j], s), s-1);
} else if (s <= 0) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.ValueNotGreaterThanZero_2, Strings.toIndexed("periods", i), s));
}
}
return Arrays.equals(coordinates, sub.coordinates) ? this : sub;
}
/**
* Returns a slice of this grid extent computed by a ratio between 0 and 1 inclusive.
* This is a helper method for {@link GridDerivation#sliceByRatio(double, int...)} implementation.
*
* @param slicePoint a pre-allocated direct position to be overwritten by this method.
* @param sliceRatio the ratio to apply on all grid dimensions except the ones to keep.
* @param dimensionsToKeep the grid dimension to keep unchanged.
*/
final GridExtent sliceByRatio(final DirectPosition slicePoint, final double sliceRatio, final int[] dimensionsToKeep) {
for (int i=slicePoint.getDimension(); --i >= 0;) {
slicePoint.setOrdinate(i, sliceRatio * getSize(i, true) + getLow(i)); // TODO: use Math.fma
}
for (int i=0; i<dimensionsToKeep.length; i++) {
slicePoint.setOrdinate(dimensionsToKeep[i], Double.NaN);
}
return slice(slicePoint, null);
}
/**
* Creates a new grid extent which represent a slice of this grid at the given point.
* The given point may have less dimensions than this grid extent, in which case the
* dimensions must be specified in the {@code modifiedDimensions} array. Coordinates
* in the given point will be rounded to nearest integer.
*
* <p>This method does not reduce the number of dimensions of the grid extent.
* For dimensionality reduction, see {@link #selectDimensions(int[])}.</p>
*
* @param slicePoint where to take a slice. NaN values are handled as if their dimensions were absent.
* @param modifiedDimensions mapping from {@code slicePoint} dimensions to this {@code GridExtent} dimensions,
* or {@code null} if {@code slicePoint} contains all grid dimensions in same order.
* @return a grid extent for the specified slice.
* @throws PointOutsideCoverageException if the given point is outside the grid extent.
*/
final GridExtent slice(final DirectPosition slicePoint, final int[] modifiedDimensions) {
final GridExtent slice = new GridExtent(this);
final int n = slicePoint.getDimension();
final int m = getDimension();
for (int k=0; k<n; k++) {
double p = slicePoint.getOrdinate(k);
if (!Double.isNaN(p)) {
final long c = Math.round(p);
final int i = (modifiedDimensions != null) ? modifiedDimensions[k] : k;
final long low = coordinates[i];
final long high = coordinates[i + m];
if (c >= low && c <= high) {
slice.coordinates[i + m] = slice.coordinates[i] = c;
} else {
final StringBuilder b = new StringBuilder();
for (int j=0; j<n; j++) {
if (j != 0) b.append(", ");
p = slicePoint.getOrdinate(j);
if (Double.isNaN(p)) b.append("NaN");
else b.append(Math.round(p));
}
throw new PointOutsideCoverageException(Resources.format(
Resources.Keys.GridCoordinateOutsideCoverage_4,
getAxisIdentification(i,k), low, high, b.toString()));
}
}
}
return Arrays.equals(coordinates, slice.coordinates) ? this : slice;
}
/**
* Creates an affine transform from the coordinates of this grid to the coordinates of the given envelope.
* This method assumes that all axes are in the same order (no axis swapping) and there is no flipping of
* axis direction except for those specified in the {@code flips} bitmask. The transform maps cell corners.
*
* @param env the target envelope. Despite this method name, the envelope CRS is ignored.
* @param flippedAxes bitmask of target axes to flip (0 if none).
* @param sourceDimensions source dimension for each target dimension, or {@code null} if dimensions are the same.
* @return an affine transform from this grid extent to the given envelope, expressed as a matrix.
*/
final MatrixSIS cornerToCRS(final Envelope env, final long flippedAxes, final int[] sourceDimensions) {
final int srcDim = getDimension();
final int tgtDim = env.getDimension();
final MatrixSIS affine = Matrices.create(tgtDim + 1, srcDim + 1, ExtendedPrecisionMatrix.CREATE_ZERO);
for (int j=0; j<tgtDim; j++) {
final int i = (sourceDimensions != null) ? sourceDimensions[j] : j;
DoubleDouble scale;
if (i < srcDim) {
final boolean flip = (flippedAxes & Numerics.bitmask(j)) != 0;
DoubleDouble offset = DoubleDouble.of(coordinates[i]);
DoubleDouble size = DoubleDouble.of(coordinates[i+srcDim]).subtract(offset).add(1);
scale = DoubleDouble.of(env.getSpan(j), true).divide(size);
if (flip) scale = scale.negate();
if (!offset.isZero()) { // Use `if` for keeping the value if scale is NaN.
offset = offset.multiply(scale).negate();
}
offset = offset.add(flip ? env.getMaximum(j) : env.getMinimum(j), true);
affine.setNumber(j, srcDim, offset);
} else {
scale = DoubleDouble.NaN;
}
affine.setNumber(j, i, scale);
}
affine.setElement(tgtDim, srcDim, 1);
return affine;
}
/**
* Returns an extent translated by the given amount of cells compared to this extent.
* The returned extent has the same {@linkplain #getSize(int) size} than this extent,
* i.e. both low and high grid coordinates are displaced by the same amount of cells.
*
* <div class="note"><b>Example:</b>
* for an extent (x: [0…10], y: [2…4], z: [0…1]) and a translation {-2, 2},
* the resulting extent would be (x: [-2…8], y: [4…6], z: [0…1]).</div>
*
* <h4>Number of arguments</h4>
* The {@code translation} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, missing values default to 0 (i.e. no translation in unspecified dimensions).
* If the array is longer, extraneous values are ignored.
*
* @param translation translation to apply on each axis in order.
* @return a grid extent whose coordinates (both low and high ones) have been translated by given amounts.
* If the given translation is a no-op (no value or only 0 ones), then this extent is returned as is.
* @throws ArithmeticException if the translation results in coordinates that overflow 64-bits integer.
*
* @see #startsAtZero()
* @see GridGeometry#shiftGrid(long...)
*
* @since 1.1
*/
public GridExtent translate(final long... translation) {
ArgumentChecks.ensureNonNull("translation", translation);
final int m = getDimension();
final int length = Math.min(m, translation.length);
if (!isZero(translation, length)) {
final GridExtent translated = new GridExtent(this);
final long[] c = translated.coordinates;
for (int i=0; i < length; i++) {
final int j = i + m;
final long t = translation[i];
c[i] = Math.addExact(c[i], t);
c[j] = Math.addExact(c[j], t);
}
return translated;
}
return this;
}
/**
* Returns {@code true} if this extent contains the given cell indices.
* An index is considered inside the grid extent if its value is between
* {@link #getLow(int) low} and {@link #getHigh(int) high} bounds, inclusive.
*
* <h4>Number of arguments</h4>
* The {@code indices} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, missing index values are considered inside the extent.
* If the array is longer, extraneous values are ignored.
*
* @param indices indices of the grid cell to check.
* @return whether the given indices are inside this extent.
*
* @since 1.2
*/
public boolean contains(final long... indices) {
ArgumentChecks.ensureNonNull("indices", indices);
final int m = getDimension();
final int length = Math.min(m, indices.length);
for (int i=0; i<length; i++) {
final long c = indices[i];
if (c < coordinates[i] || c > coordinates[i + m]) {
return false;
}
}
return true;
}
/**
* Returns the intersection of this grid extent with the given grid extent.
* The given extent shall have the same number of dimensions than this extent.
* The {@linkplain #getAxisType(int) axis types} (vertical, temporal, …) must
* be the same in all dimensions, ignoring types that are absent.
*
* @param other the grid to intersect with.
* @return the intersection result. May be one of the existing instances.
* @throws MismatchedDimensionException if the two extents do not have the same number of dimensions.
* @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension.
* @throws DisjointExtentException if the given extent does not intersect this extent.
*
* @since 1.3
*/
public GridExtent intersect(final GridExtent other) {
return combine(other, false);
}
/**
* Returns the union of this grid extent with the given grid extent.
* The given extent shall have the same number of dimensions than this extent.
* The {@linkplain #getAxisType(int) axis types} (vertical, temporal, …) must
* be the same in all dimensions, ignoring types that are absent.
*
* @param other the grid to combine with.
* @return the union result. May be one of the existing instances.
* @throws MismatchedDimensionException if the two extents do not have the same number of dimensions.
* @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension.
*
* @since 1.3
*/
public GridExtent union(final GridExtent other) {
return combine(other, true);
}
/**
* Implementation of {@link #union(GridExtent)} and {@link #intersect(GridExtent)}
*
* @param other the grid to combine with.
* @return the union or intersection result, or {@code null} if the intersection gave an empty result.
* @throws MismatchedDimensionException if the two extents do not have the same number of dimensions.
* @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension.
*/
private GridExtent combine(final GridExtent other, final boolean union) {
final int n = coordinates.length;
final int m = n >>> 1;
if (n != other.coordinates.length) {
throw new MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_3, "other", m, other.getDimension()));
}
// First condition below is a fast check for a common case.
if (types != other.types && types != null && other.types != null) {
for (int i=0; i<m; i++) {
final DimensionNameType t1 = types[i];
if (t1 != null) {
final DimensionNameType t2 = other.types[i];
if (t2 != null && !t1.equals(t2)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedAxes_3, i, t1, t2));
}
}
}
}
final long[] clipped = new long[n];
int i = 0;
while (i < m) {clipped[i] = extremum(coordinates[i], other.coordinates[i], !union); i++;}
while (i < n) {clipped[i] = extremum(coordinates[i], other.coordinates[i], union); i++;}
if (Arrays.equals(clipped, this.coordinates)) return this;
if (Arrays.equals(clipped, other.coordinates)) return other;
if (!union) {
for (i=0; i<m; i++) {
if (clipped[i] > clipped[i+m]) {
throw new DisjointExtentException(this, other, i);
}
}
}
return new GridExtent(this, clipped);
}
/**
* Returns the minimum or maximum value between the given pair of values.
*/
private static long extremum(final long a, final long b, final boolean max) {
return max ? Math.max(a, b) : Math.min(a, b);
}
/**
* Returns whether this grid extent has the same size than the given extent.
* If the given extent is {@code null} or has a different number of dimensions,
* then this method returns {@code false}.
*
* <p>This method is not public because we do not yet have a policy
* about whether we should verify if axis {@link #types} match.</p>
*
* @param other the other extent to compare with this extent. Can be {@code null}.
* @return whether the two extents has the same size.
*/
final boolean isSameSize(final GridExtent other) {
if (other == null || coordinates.length != other.coordinates.length) {
return false;
}
final int dimension = getDimension();
final long[] oc = other.coordinates;
for (int i=0; i<dimension; i++) {
if (coordinates[i+dimension] - coordinates[i] != oc[i+dimension] - oc[i]) {
return false;
}
}
return true;
}
/**
* Compares the specified object with this grid extent for equality.
* This method delegates to {@code equals(object, ComparisonMode.STRICT)}.
*
* @param object the object to compare with this grid extent for equality.
* @return {@code true} if the given object is equal to this grid extent.
*/
@Override
public final boolean equals(final Object object) {
return equals(object, ComparisonMode.STRICT);
}
/**
* Compares the specified object with this grid extent for equality.
* If the mode is {@link ComparisonMode#IGNORE_METADATA} or more flexible,
* then the {@linkplain #getAxisType(int) axis types} are ignored.
*
* @param object the object to compare with this grid extent for equality.
* @param mode the strictness level of the comparison.
* @return {@code true} if the given object is equal to this grid extent.
*
* @since 1.1
*/
@Override
@SuppressWarnings("fallthrough")
public boolean equals(final Object object, final ComparisonMode mode) {
if (object == this) {
return true;
}
if (object instanceof GridExtent) {
final GridExtent other = (GridExtent) object;
if (Arrays.equals(coordinates, other.coordinates)) {
switch (mode) {
case STRICT: if (!getClass().equals(object.getClass())) return false; // else fallthrough
case BY_CONTRACT: if (!Arrays.equals(types, other.types)) return false; // else fallthrough
default: return true;
}
}
}
return false;
}
/**
* Returns a hash value for this grid extent. This value needs not to remain
* consistent between different implementations of the same class.
*
* @return a hash value for this grid extent.
*/
@Override
public int hashCode() {
return Arrays.hashCode(coordinates) + Arrays.hashCode(types) ^ (int) serialVersionUID;
}
/**
* Returns a string representation of this grid extent. The returned string
* is implementation dependent and is provided for debugging purposes only.
*/
@Override
public String toString() {
final StringBuilder out = new StringBuilder(256);
try {
appendTo(out, Vocabulary.getResources((Locale) null));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return out.toString();
}
/**
* Writes a string representation of this grid extent in the given buffer.
* This method is provided for allowing caller to recycle the same buffer.
*
* @param out where to write the string representation.
* @param vocabulary resources for some words.
*/
final void appendTo(final Appendable out, final Vocabulary vocabulary) throws IOException {
final TableAppender table = new TableAppender(out, "");
final int dimension = getDimension();
for (int i=0; i<dimension; i++) {
String name = null;
if (types != null) {
final InternationalString title = Types.getCodeTitle(types[i]);
if (title != null) {
name = title.toString(vocabulary.getLocale());
}
}
if (name == null) {
name = vocabulary.getString(Vocabulary.Keys.Dimension_1, i);
}
final long lower = coordinates[i];
final long upper = coordinates[i + dimension];
table.setCellAlignment(TableAppender.ALIGN_LEFT);
table.append(name).append(": ").nextColumn();
table.append('[').nextColumn();
table.setCellAlignment(TableAppender.ALIGN_RIGHT);
table.append(Long.toString(lower)).append(" … ").nextColumn();
table.append(Long.toString(upper)).append("] ") .nextColumn();
table.append('(').append(vocabulary.getString(Vocabulary.Keys.CellCount_1,
toSizeString(upper - lower + 1))).append(')').nextLine();
}
table.flush();
}
/**
* Returns a string representation of the given size, assumed computed by {@code high - low + 1}.
* A value of 0 means that there is an overflow and that the true value os 2<sup>64</sup>.
*/
static String toSizeString(final long size) {
return (size != 0) ? Long.toUnsignedString(size) : "2⁶⁴";
}
}