blob: 2e310b48837cb503f682ba004d1c7af699b82445 [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.referencing.operation.transform;
import java.util.Arrays;
import java.io.Serializable;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.referencing.operation.matrix.Matrix1;
import org.apache.sis.internal.referencing.provider.Interpolation1D;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.resources.Errors;
/**
* A transform that performs linear interpolation between values.
* The transform is invertible if, and only if, the values are in increasing order.
*
* <p>If desired values in decreasing order can be supported by inverting the sign of all values,
* then concatenating this transform with a transform that multiply all output values by -1.</p>
*
* <div class="section">Extrapolation</div>
* If an input value is outside the expected range of values, this class extrapolates using the
* slope defined by the two first points if the requested value is before, or the slope defined
* by the two last points if the requested value is after. In other words, extrapolations are
* computed using only values at the extremum where extrapolation happen. This rule causes less
* surprising behavior when computing a data cube envelope, which may need extrapolation by 0.5
* pixel before the first value or after the last value.
*
* <div class="note"><b>Example:</b>
* if a vertical dimension is made of slices at y₀=5, y₁=10, y₂=100 and y₃=250 meters, then linear
* interpolation at 0.5 is 7.5 meters and extrapolation at -0.5 is expected to give 2.5 meters.</div>
*
* @author Johann Sorel (Geomatys)
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
*
* @see MathTransforms#interpolate(double[], double[])
*
* @since 0.7
* @module
*/
final class LinearInterpolator1D extends AbstractMathTransform1D implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -5025693608589996896L;
/**
* The sequence values specified at construction time.
* Must contain at least 2 values.
*/
private final double[] values;
/**
* If the transform is invertible, the inverse. Otherwise {@code null}.
* The transform is invertible only if values are in increasing order.
*/
private final MathTransform1D inverse;
/**
* Creates a new transform which will interpolate in the given table of values.
* The inputs are {0, 1, … , <var>N</var>} where <var>N</var> is length of output values.
*
* <p>This constructor assumes that the {@code values} array has already been cloned,
* so it will not clone it again. That array shall contain at least two values.</p>
*
* @param values the <var>y</var> values in <var>y=f(x)</var> where <var>x</var> = {0, 1, … , {@code values.length-1}}.
*/
private LinearInterpolator1D(final double[] values) {
this.values = values; // Cloning this array is caller's responsibility.
double last = values[0];
for (int i=1; i<values.length; i++) {
if (!(last <= (last = values[i]))) { // Use '!' for catching NaN values.
inverse = null; // Transform is not reversible.
return;
}
}
inverse = new Inverse(this);
}
/**
* Creates a transform for the given values. This method returns an affine transform instead than an
* interpolator if the given values form a series with a constant increment. The given array shall
* contain at least two values.
*
* @param values a <strong>copy</strong> of the user-provided values. This array may be modified.
*/
private static MathTransform1D create(final double[] values) {
final int n = values.length - 1;
final double offset = values[0];
final double slope = (values[n] - offset) / n;
final double as = Math.abs(slope);
/*
* If the increment between values is constant (with a small tolerance factor),
* return a one-dimensional affine transform instead than an interpolator.
* We need to perform this check before the sign reversal applied after this loop.
*/
double value;
int i = 0;
do {
if (++i >= n) {
return LinearTransform1D.create(slope, offset);
}
value = values[i];
} while (Numerics.epsilonEqual(value, offset + slope*i, // TODO: use Math.fma with JDK9.
Math.max(Math.abs(value), as) * Numerics.COMPARISON_THRESHOLD));
/*
* If the values are in decreasing order, reverse their sign so we get increasing order.
* We will multiply the results by -1 after the transformation.
*/
final boolean isReverted = (slope < 0);
if (isReverted) {
for (i=0; i <= n; i++) {
values[i] = -values[i];
}
}
MathTransform1D tr = new LinearInterpolator1D(values);
if (isReverted) {
tr = new ConcatenatedTransformDirect1D(tr, LinearTransform1D.NEGATE);
}
return tr;
}
/**
* Creates a <i>y=f(x)</i> transform for the given preimage (<var>x</var>) and values (<var>y</var>).
* See {@link MathTransforms#interpolate(double[], double[])} javadoc for more information.
*/
static MathTransform1D create(final double[] preimage, final double[] values) {
final int length;
if (preimage == null) {
if (values == null) {
return IdentityTransform1D.INSTANCE;
}
length = values.length;
} else {
length = preimage.length;
if (values != null && values.length != length) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedArrayLengths));
}
}
switch (length) {
case 0: throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, (preimage != null) ? "preimage" : "values"));
case 1: return LinearTransform1D.constant((preimage != null) ? preimage[0] : Double.NaN, (values != null) ? values[0] : Double.NaN);
}
/*
* A common usage of this 'create' method is for creating a "gridToCRS" transform from grid coordinates
* to something else, in which case the preimage array is null. In the less frequent case where preimage
* is non-null, we first convert from preimage to indices, then from indices to y values.
*/
MathTransform1D tr = null;
if (values != null) {
tr = create(values.clone());
}
if (preimage != null) {
final MathTransform1D indexToValues = tr;
try {
tr = create(preimage.clone()).inverse(); // preimageToIndex transform.
} catch (NoninvertibleTransformException e) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.NonMonotonicSequence_1, "preimage"), e);
}
if (indexToValues != null) {
tr = MathTransforms.concatenate(tr, indexToValues);
}
}
return tr;
}
/**
* Returns the parameter descriptors for this math transform.
*/
@Override
public ParameterDescriptorGroup getParameterDescriptors() {
return Interpolation1D.PARAMETERS;
}
/**
* Returns the parameter values for this math transform.
*/
@Override
public ParameterValueGroup getParameterValues() {
final ParameterValueGroup p = getParameterDescriptors().createValue();
p.parameter("values").setValue(values);
return p;
}
/**
* Returns {@code true} if this transform is the identity transform. This method should never returns {@code true}
* since we verified the inputs in the {@code create(…)} method. We nevertheless verify as a paranoiac safety.
*/
@Override
public boolean isIdentity() {
for (int i=0; i<values.length; i++) {
if (values[i] != i) return false;
}
return true;
}
/**
* Combines {@link #transform(double)}, {@link #derivative(double)} in a single method call.
*/
@Override
public Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate) throws TransformException
{
double x = srcPts[srcOff];
final double y, d;
if (x >= 0) {
final int i = (int) x;
final int n = values.length - 1;
if (i < n) {
x -= i;
final double y0 = values[i ];
final double y1 = values[i+1];
y = y0 * (1-x) + y1 * x;
d = y1 - y0;
} else {
// x is after the last available value.
final double y1 = values[n];
d = y1 - values[n-1];
y = (x - n) * d + y1;
}
} else {
// x is before the first available value.
final double y0 = values[0];
d = values[1] - y0;
y = x * d + y0;
}
if (dstPts != null) {
dstPts[dstOff] = y;
}
return derivate ? new Matrix1(d) : null;
}
/**
* Interpolates a <var>y</var> values for the given <var>x</var>.
* The given <var>x</var> value should be between 0 to {@code values.length - 1} inclusive.
* If the given input value is outside that range, then the output value will be extrapolated.
*/
@Override
public double transform(double x) {
if (x >= 0) {
final int i = (int) x;
final int n = values.length - 1;
if (i < n) {
x -= i;
return values[i] * (1-x) + values[i+1] * x;
} else {
// x is after the last available value.
final double y1 = values[n];
return (x - n) * (y1 - values[n-1]) + y1;
}
} else {
// x is before the first available value.
final double y0 = values[0];
return x * (values[1] - y0) + y0;
}
}
/**
* Returns the derivative of <var>y</var> for the given <var>x</var>.
* Note: for each segment, the derivative is considered constant between
* <var>x</var> inclusive and <var>x+1</var> exclusive.
*/
@Override
public double derivative(final double x) {
final int i = Math.max(0, Math.min(values.length - 2, (int) x));
return values[i+1] - values[i];
}
/**
* Returns the inverse of this transform, or throw an exception if there is no inverse.
*/
@Override
public MathTransform1D inverse() throws NoninvertibleTransformException {
return (inverse != null) ? inverse : super.inverse();
}
/**
* The inverse of the enclosing {@link LinearInterpolator1D}. Given a <var>y</var> value, this class performs
* a bilinear search for locating the lower and upper <var>x</var> values as integers, then interpolates the
* <var>x</var> real value.
*/
private static final class Inverse extends AbstractMathTransform1D.Inverse implements MathTransform1D, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -5112948223332095009L;
/**
* The enclosing transform.
*/
private final LinearInterpolator1D forward;
/**
* Creates a new inverse transform.
*/
Inverse(final LinearInterpolator1D forward) {
this.forward = forward;
}
/**
* Returns the inverse of this math transform.
*/
@Override
public MathTransform1D inverse() {
return forward;
}
/**
* Combines {@link #transform(double)}, {@link #derivative(double)} in a single method call.
* The intent is to avoid to call {@link Arrays#binarySearch(double[], double)} twice for the
* same value.
*/
@Override
public Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate) throws TransformException
{
final double d, x, y = srcPts[srcOff];
final double[] values = forward.values;
int i = Arrays.binarySearch(values, y);
if (i >= 0) {
x = i;
i = Math.max(1, Math.min(values.length - 1, i));
d = values[i] - values[i-1];
} else {
i = ~i;
if (i >= 1) {
if (i < values.length) {
final double y0 = values[i-1];
x = (y - y0) / (d = values[i] - y0) + (i-1);
} else {
// y is after the last available value.
final int n = values.length - 1;
final double y1 = values[n];
x = (y - y1) / (d = y1 - values[n-1]) + n;
}
} else {
// y is before the first available value.
final double y0 = values[0];
x = (y - y0) / (d = values[1] - y0);
}
}
if (dstPts != null) {
dstPts[dstOff] = x;
}
return derivate ? new Matrix1(1/d) : null;
}
/**
* Locates by bilinear search and interpolates the <var>x</var> value for the given <var>y</var>.
*/
@Override
public double transform(final double y) {
final double[] values = forward.values;
int i = Arrays.binarySearch(values, y);
if (i >= 0) {
return i;
} else {
i = ~i;
if (i >= 1) {
if (i < values.length) {
final double y0 = values[i-1];
return (y - y0) / (values[i] - y0) + (i-1);
} else {
// y is after the last available value.
final int n = values.length - 1;
final double y1 = values[n];
return (y - y1) / (y1 - values[n-1]) + n;
}
} else {
// y is before the first available value.
final double y0 = values[0];
return (y - y0) / (values[1] - y0);
}
}
}
/**
* Returns the derivative at the given <var>y</var> value.
*/
@Override
public double derivative(final double y) {
final double[] values = forward.values;
int i = Arrays.binarySearch(values, y);
if (i < 0) {
i = ~i;
}
i = Math.max(1, Math.min(values.length - 1, i));
return 1 / (values[i] - values[i-1]);
}
}
/**
* Computes a hash code value for this transform.
*/
@Override
protected int computeHashCode() {
return super.computeHashCode() ^ Arrays.hashCode(values);
}
/**
* Compares this transform with the given object for equality.
*/
@Override
public boolean equals(final Object object, final ComparisonMode mode) {
if (super.equals(object, mode)) {
// No need to compare 'slope' because it is computed from 'values'.
return Arrays.equals(values, ((LinearInterpolator1D) object).values);
}
return false;
}
}