blob: 5cf4dfb9feafb5d8ea173a5d9e27ee33b8a8da45 [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.Arrays;
import java.io.Serializable;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.util.StringBuilders;
import org.apache.sis.util.privy.Strings;
import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.coordinate.MismatchedDimensionException;
import org.opengis.coverage.PointOutsideCoverageException;
import org.opengis.coverage.grid.GridCoordinates;
/**
* Grid coordinates which may have fraction digits after the integer part.
* Grid coordinates specify the location of a cell within a {@link GridCoverage}.
* They are normally integer numbers, but fractional parts may exist for example
* after converting a geospatial {@link DirectPosition} to grid coordinates.
* Preserving that fractional part is needed for interpolations.
* This class can store such fractional part and can also compute a {@link GridExtent}
* containing the coordinates, which can be used for requesting data for interpolations.
*
* <p>Current implementation stores coordinate values as {@code double} precision floating-point numbers
* and {@linkplain Math#round(double) rounds} them to 64-bits integers on the fly. If a {@code double}
* cannot be {@linkplain #getCoordinateValue(int) returned} as a {@code long}, or if a {@code long}
* cannot be {@linkplain #setCoordinateValue(int, long) stored} as a {@code double}, then an
* {@link ArithmeticException} is thrown.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.5
*
* @see GridCoverage.Evaluator#toGridCoordinates(DirectPosition)
*
* @since 1.1
*/
public class FractionalGridCoordinates implements GridCoordinates, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 5652265407347129550L;
/**
* The grid coordinates as floating-point numbers.
*/
final double[] coordinates;
/**
* Creates a new grid coordinates with the given number of dimensions.
*
* <h4>Usage note</h4>
* {@code FractionalGridCoordinates} are usually not created directly, but are instead obtained
* indirectly for example from the {@linkplain GridCoverage.Evaluator#toGridCoordinates(DirectPosition)
* conversion of a geospatial position}.
*
* @param dimension the number of dimensions.
*/
public FractionalGridCoordinates(final int dimension) {
coordinates = new double[dimension];
}
/**
* Creates a new grid coordinates initialized to a copy of the given coordinates.
*
* @param other the coordinates to copy.
*/
public FractionalGridCoordinates(final FractionalGridCoordinates other) {
coordinates = other.coordinates.clone();
}
/**
* Returns the number of dimension of this grid coordinates.
*
* @return the number of dimensions.
*/
@Override
public int getDimension() {
return coordinates.length;
}
/**
* Returns one integer value for each dimension of the grid.
* The default implementation invokes {@link #getCoordinateValue(int)}
* for each element in the returned array.
*
* @return a copy of the coordinates. Changes in the returned array will
* not be reflected back in this {@code GridCoordinates} object.
* @throws ArithmeticException if a coordinate value is outside the range
* of values representable as a 64-bits integer value.
*/
@Override
public long[] getCoordinateValues() {
final long[] indices = new long[coordinates.length];
for (int i=0; i<indices.length; i++) {
indices[i] = getCoordinateValue(i);
}
return indices;
}
/**
* Returns the grid coordinate value at the specified dimension.
* Floating-point values are rounded to the nearest 64-bits integer values.
* If the coordinate value is NaN or outside the range of {@code long} values,
* then an {@link ArithmeticException} is thrown.
*
* @param dimension the dimension for which to obtain the coordinate value.
* @return the coordinate value at the given dimension,
* {@linkplain Math#round(double) rounded} to nearest integer.
* @throws IndexOutOfBoundsException if the given index is negative or is
* equal or greater than the {@linkplain #getDimension grid dimension}.
* @throws ArithmeticException if the coordinate value is outside the range
* of values representable as a 64-bits integer value.
*/
@Override
public long getCoordinateValue(final int dimension) {
final double value = coordinates[dimension];
/*
* 2048 is the smallest value that can be added or removed to Long.MIN/MAX_VALUE,
* as given by Math.ulp(Long.MIN_VALUE). We add this tolerance since the contract
* is to return the `long` value closest to the `double` value and we consider a
* 1 ULP error as close enough.
*/
if (value >= (Long.MIN_VALUE - 2048d) && value <= (Long.MAX_VALUE + 2048d)) {
return Math.round(value);
}
throw new ArithmeticException(Resources.format(Resources.Keys.UnconvertibleGridCoordinate_2, "long", value));
}
/**
* Returns a grid coordinate value together with its fractional part, if any.
*
* @param dimension the dimension for which to obtain the coordinate value.
* @return the 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}.
*/
public double getCoordinateFractional(final int dimension) {
return coordinates[dimension];
}
/**
* Sets the coordinate value at the specified dimension.
* The given value shall be convertible to {@code double} without precision lost.
*
* @param dimension the dimension for which to set the coordinate value.
* @param value the new value.
* @throws IndexOutOfBoundsException if the given index is negative or is
* equal or greater than the {@linkplain #getDimension grid dimension}.
* @throws ArithmeticException if this method cannot store the given grid coordinate
* without precision lost.
*/
@Override
public void setCoordinateValue(final int dimension, final long value) {
if ((coordinates[dimension] = value) != value) {
throw new ArithmeticException(Resources.format(Resources.Keys.UnconvertibleGridCoordinate_2, "double", value));
}
}
/**
* Creates a new grid extent around this grid coordinates. The returned extent will have the same number
* of dimensions than this grid coordinates. For each dimension <var>i</var> the following relationships
* will hold:
*
* <ol>
* <li>If <code>extent.{@linkplain GridExtent#getSize(int) getSize}(i)</code> ≥ 2 and no shift (see below) then:<ul>
* <li><code>extent.{@linkplain GridExtent#getLow(int) getLow}(i)</code> ≤
* <code>{@linkplain #getCoordinateFractional(int) getCoordinateFractional}(i)</code></li>
* <li><code>extent.{@linkplain GridExtent#getHigh(int) getHigh}(i)</code> ≥
* <code>{@linkplain #getCoordinateFractional(int) getCoordinateFractional}(i)</code></li>
* </ul></li>
* <li>If {@code bounds.getSize(i)} ≥ {@code size[i]} and {@code size[i]} ≠ 0 then:<ul>
* <li><code>extent.{@linkplain GridExtent#getSize(int) getSize}(i)</code> = {@code size[i]}</li>
* </ul></li>
* </ol>
*
* <p>The {@code size} argument is optional and can be incomplete (i.e. the number of {@code size} values can be
* less than the number of dimensions). For each dimension <var>i</var>, if a {@code size[i]} value is provided
* and is not zero, then this method tries to expand the extent in that dimension to the specified {@code size[i]}
* value as shown in constraint #2 above. Otherwise the default size is the smallest possible extent that met
* constraint #1 above, clipped to the {@code bounds}. This implies a size of 1 if the grid coordinate in that
* dimension is an integer, or a size of 2 (before clipping to the bounds) if the grid coordinate has a fractional
* part.</p>
*
* <p>The {@code bounds} argument is also optional.
* If non-null, then this method enforces the following additional rules:</p>
*
* <ul>
* <li>Coordinates rounded to nearest integers must be inside the given bounds,
* otherwise a {@link PointOutsideCoverageException} is thrown.</li>
* <li>If the computed extent overlaps an area outside the bounds, then the extent will be shifted (if an explicit
* size was given) or clipped (if automatic size is used) in order to be be fully contained inside the bounds.</li>
* <li>If a given size is larger than the corresponding bounds {@linkplain GridExtent#getSize(int) size},
* then the returned extent will be clipped to the bounds.</li>
* </ul>
*
* <p>In all cases, this method tries to keep the grid coordinates close to the center of the returned extent.
* A shift may exist if necessary for keeping the extent inside the {@code bounds} argument, but will never
* move the grid coordinates outside the [<var>low</var> … <var>high</var>+1) range of returned extent.</p>
*
* @param bounds if the coordinates shall be contained inside a grid, that grid extent. Otherwise {@code null}.
* @param size the desired extent sizes as strictly positive numbers, or 0 sentinel values for automatic
* sizes (1 or 2 depending on bounds and coordinate values). This array may have any length;
* if shorter than the number of dimensions, missing values default to 0.
* If longer than the number of dimensions, extra values are ignored.
* @throws IllegalArgumentException if a {@code size} value is negative.
* @throws ArithmeticException if a coordinate value is outside the range of {@code long} values.
* @throws MismatchedDimensionException if {@code bounds} dimension is not equal to grid coordinates dimension.
* @throws PointOutsideCoverageException if the grid coordinates (rounded to nearest integers) are outside the
* given bounds.
* @return a grid extent of the given size (if possible) containing those grid coordinates.
*/
public GridExtent toExtent(final GridExtent bounds, final long... size) {
return toExtent(bounds, size, false);
}
/**
* Implementation of {@link #toExtent(GridExtent, long...)} with the option to replace
* {@link PointOutsideCoverageException} by null return value.
*
* @param bounds if the coordinates shall be contained inside a grid, that grid extent. Otherwise {@code null}.
* @param size the desired extent sizes as strictly positive numbers, or 0 for automatic sizes (1 or 2).
* @param nullIfOutside whether to return {@code null} instead of throwing an exception if given point is outside coverage bounds.
* @return a grid extent containing grid coordinates, or {@code null} if outside and {@code nullIfOutside} is {@code true}.
*/
final GridExtent toExtent(final GridExtent bounds, final long[] size, final boolean nullIfOutside) {
final int dimension = coordinates.length;
if (bounds != null) {
final int bd = bounds.getDimension();
if (bd != dimension) {
throw new MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_3, "bounds", dimension, bd));
}
}
final long[] extent = GridExtent.allocate(dimension);
for (int i=0; i<dimension; i++) {
final double value = coordinates[i];
if (!(value >= Long.MIN_VALUE && value <= Long.MAX_VALUE)) { // Use ! for catching NaN values.
throw new ArithmeticException(Resources.format(
Resources.Keys.UnconvertibleGridCoordinate_2, "long", value));
}
long margin = 0;
if (i < size.length) {
margin = size[i];
if (margin < 0) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.NegativeArgument_2, Strings.toIndexed("size", i), margin));
}
}
/*
* The lower/upper values are given by Math.floor/ceil respectively (may be equal).
* However, we do an exception to this rule if user asked explicitly for a size of 1.
* In such case we can no longer enforce the `lower ≤ value ≤ upper` rule. The best
* we can do is to take the nearest neighbor.
*/
final long nearest = Math.round(value);
long lower, upper;
if (margin == 1) {
lower = upper = nearest;
} else {
lower = (long) Math.floor(value); // Inclusive.
upper = (long) Math.ceil (value); // Inclusive too (lower == upper if value is an integer).
if (margin != 0) {
margin -= (upper - lower + 1); // Total number of cells to add.
assert margin >= 0 : margin; // Because (upper - lower + 1) ≤ 2
if ((margin & 1) != 0) {
if (nearest >= upper) {
upper = Math.incrementExact(upper);
} else {
lower = Math.decrementExact(lower);
}
}
margin >>= 1; // Number of cells to add on each side.
lower = Math.subtractExact(lower, margin);
upper = Math.addExact(upper, margin);
margin = 2; // Any value different than 0 for remembering that it was explicitly specified.
}
}
/*
* At this point the grid range has been computed (lower to upper).
* Shift it if needed for keeping it inside the enclosing extent.
*/
if (bounds != null) {
final long validMin = bounds.getLow(i);
final long validMax = bounds.getHigh(i);
if (nearest > validMax || nearest < validMin) {
if (nullIfOutside) {
return null;
}
final StringBuilder b = new StringBuilder();
writeCoordinates(b);
throw new PointOutsideCoverageException(
Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4,
bounds.getAxisIdentification(i,i), validMin, validMax, b.toString()));
}
if (upper > validMax) {
if (margin != 0) { // In automatic mode (margin = 0) just clip, don't shift.
/*
* Because (upper - validMax) is always positive, then (t > lower) would mean
* that we have an overflow. In such cases we do not need the result since we
* know that we are outside the enclosing extent anyway.
*/
final long t = lower - Math.subtractExact(upper, validMax);
lower = (t >= validMin && t <= lower) ? t : validMin;
}
upper = validMax;
}
if (lower < validMin) {
if (margin != 0) {
final long t = upper + Math.subtractExact(validMin, lower);
upper = (t <= validMax && t >= upper) ? t : validMax; // Same rational as above.
}
lower = validMin;
}
}
extent[i] = lower;
extent[i+dimension] = upper;
}
return new GridExtent(bounds, extent);
}
/**
* Returns the grid coordinates converted to a geospatial position using the given transform.
* The {@code gridToCRS} argument is typically {@link GridGeometry#getGridToCRS(PixelInCell)}
* with {@link PixelInCell#CELL_CENTER}.
*
* @param gridToCRS the transform to apply on grid coordinates.
* @return the grid coordinates converted using the given transform.
* @throws TransformException if the grid coordinates cannot be converted by {@code gridToCRS}.
*
* @see GridCoverage.Evaluator#toGridCoordinates(DirectPosition)
*/
public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
return gridToCRS.transform(new Position(this), null);
}
/**
* A grid coordinates viewed as a {@link DirectPosition}. This class is used only for coordinate transformation.
* We do not want to make this class public in order to avoid the abuse of {@link DirectPosition} as a storage
* of grid coordinates.
*
* <p>Note this this class does not comply with the contract documented in {@link DirectPosition#equals(Object)}
* and {@link DirectPosition#hashCode()} javadoc. This is another reason for not making this class public.</p>
*/
static final class Position extends FractionalGridCoordinates implements DirectPosition {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -7804151694395153401L;
/**
* Creates a new position of the given number of dimensions.
*/
Position(final int dimension) {
super(dimension);
}
/**
* Creates a new position initialized to a copy of the given coordinates.
*/
Position(final FractionalGridCoordinates other) {
super(other);
}
/**
* Returns all coordinate values.
*/
@Override
public double[] getCoordinates() {
return coordinates.clone();
}
/**
* Returns the coordinate value at the given dimension.
*/
@Override
public double getCoordinate(int dimension) {
return coordinates[dimension];
}
/**
* Sets the coordinate value at the given dimension.
*/
@Override
public void setCoordinate(final int dimension, final double value) {
coordinates[dimension] = value;
}
/**
* Returns the grid coordinates converted to a geospatial position using the given transform.
*/
@Override
public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
return gridToCRS.transform(this, null);
}
}
/**
* Creates an error message for a grid coordinates out of bounds.
* This method tries to detect the dimension of the out-of-bounds
* coordinate by searching for the dimension with largest error.
*
* @param bounds the expected bounds, or {@code null} if unknown.
* @return message to provide to {@link PointOutsideCoverageException},
* or {@code null} if the given bounds were null.
*/
final String pointOutsideCoverage(final GridExtent bounds) {
if (bounds == null) {
return null;
}
int axis = 0;
long validMin = 0;
long validMax = 0;
double distance = 0;
for (int i = bounds.getDimension(); --i >= 0;) {
final long low = bounds.getLow(i);
final long high = bounds.getHigh(i);
final double c = coordinates[i];
double d = low - c;
if (!(d > distance)) { // Use '!' for entering in this block if `d` is NaN.
d = c - high;
if (!(d > distance)) { // Use '!' for skipping this coordinate if `d` is NaN.
continue;
}
}
axis = i;
validMin = low;
validMax = high;
distance = d;
}
final StringBuilder b = new StringBuilder();
writeCoordinates(b);
return Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4,
bounds.getAxisIdentification(axis, axis), validMin, validMax, b.toString());
}
/**
* Returns a string representation of this grid coordinates for debugging purpose.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder("GridCoordinates[");
writeCoordinates(buffer);
return buffer.append(']').toString();
}
/**
* Writes coordinates in the given buffer.
*/
private void writeCoordinates(final StringBuilder buffer) {
for (int i=0; i<coordinates.length; i++) {
if (i != 0) buffer.append(' ');
StringBuilders.trimFractionalPart(buffer.append(coordinates[i]));
}
}
/**
* Returns a hash code value for this grid coordinates.
*/
@Override
public int hashCode() {
return Arrays.hashCode(coordinates) ^ (int) serialVersionUID;
}
/**
* Compares this grid coordinates with the specified object for equality.
*
* @param object the object to compares with this grid coordinates.
* @return {@code true} if the given object is equal to this grid coordinates.
*/
@Override
public boolean equals(final Object object) {
if (object == this) { // Slight optimization.
return true;
}
if (object != null && object.getClass() == getClass()) {
return Arrays.equals(((FractionalGridCoordinates) object).coordinates, coordinates);
}
return false;
}
}