| /* |
| * 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.measure; |
| |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| import javax.measure.UnitConverter; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.StringBuilders; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.LenientComparable; |
| import org.apache.sis.math.MathFunctions; |
| import org.apache.sis.math.Fraction; |
| import org.apache.sis.util.internal.Numerics; |
| import org.apache.sis.util.internal.DoubleDouble; |
| |
| |
| /** |
| * Conversions between units that can be represented by a linear operation (scale or offset). |
| * Note that the "linear" word in this class does not have the same meaning than the same word |
| * in the {@link #isLinear()} method inherited from JSR-385. |
| * |
| * <h2>Implementation note</h2> |
| * for performance reason we should create specialized subtypes for the case where there is only a scale to apply, |
| * or only an offset, <i>etc.</i> But we don't do that in Apache SIS implementation because we will rarely use the |
| * {@code UnitConverter} for converting a lot of values. We rather use {@code MathTransform} for operations on |
| * <var>n</var>-dimensional tuples, and unit conversions are only a special case of those more generic operations. |
| * The {@code org.apache.sis.referencing} module provided the specialized implementations needed for efficient |
| * operations and know how to copy the {@code UnitConverter} coefficients into an affine transform matrix. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| final class LinearConverter extends AbstractConverter implements LenientComparable { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = -3759983642723729926L; |
| |
| /** |
| * The scale to apply for converting values, before division by {@link #divisor}. |
| */ |
| private final double scale; |
| |
| /** |
| * The offset to apply after the scale, before division by {@link #divisor}. |
| */ |
| private final double offset; |
| |
| /** |
| * A divisor applied after the conversion. |
| * The complete formula used by Apache SIS is {@code y = (x*scale + offset) / divisor}. |
| * This division is mathematically unneeded since we could divide the offset and scale factor directly, |
| * but we keep it for accuracy reasons because most unit conversion factors are defined in base 10 and |
| * IEEE 754 cannot represent fractional values in base 10 accurately. |
| */ |
| private final double divisor; |
| |
| /** |
| * The scale and offset factors represented in base 10, computed when first needed. |
| * Those terms are pre-divided by the {@linkplain #divisor}. |
| */ |
| private transient volatile BigDecimal scale10, offset10; |
| |
| /** |
| * The inverse of this unit converter. Computed when first needed. |
| */ |
| private transient volatile LinearConverter inverse; |
| |
| /** |
| * Creates a new linear converter for the given scale and offset. |
| * The complete formula applied is {@code y = (x*scale + offset) / divisor}. |
| */ |
| LinearConverter(final double scale, final double offset, final double divisor) { |
| this.scale = scale; |
| this.offset = offset; |
| this.divisor = divisor; |
| } |
| |
| /** |
| * Creates a linear converter from the given scale and offset, which may be {@link BigDecimal} instances. |
| * This is the implementation of public {@link Units#converter(Number, Number)} method. |
| */ |
| static AbstractConverter create(final Number scale, final Number offset) { |
| final double numerator, divisor; |
| double shift = (offset != null) ? doubleValue(offset) : 0; |
| if (scale instanceof Fraction) { |
| numerator = ((Fraction) scale).numerator; |
| divisor = ((Fraction) scale).denominator; |
| shift *= divisor; |
| } else { |
| numerator = (scale != null) ? doubleValue(scale) : 1; |
| divisor = 1; |
| } |
| if (shift == 0 && numerator == divisor) { |
| return IdentityConverter.INSTANCE; |
| } |
| final LinearConverter c = new LinearConverter(numerator, shift, divisor); |
| if (scale instanceof BigDecimal) c.scale10 = (BigDecimal) scale; |
| if (offset instanceof BigDecimal) c.offset10 = (BigDecimal) offset; |
| return c; |
| } |
| |
| /** |
| * Returns a linear converter for the given ratio. The scale factor is specified as a ratio because |
| * the unit conversion factors are often defined with a value in base 10. That value is considered |
| * exact by definition, but IEEE 754 has no exact representation of decimal fraction digits. |
| * |
| * <p>It is caller's responsibility to skip this method call when {@code numerator} = {@code denominator}. |
| * This method does not perform this check because it is usually already done (indirectly) by the caller.</p> |
| */ |
| static LinearConverter scale(final double numerator, final double denominator) { |
| return new LinearConverter(numerator, 0, denominator); |
| } |
| |
| /** |
| * Returns a converter for the given shift. The translation is specified as a fraction because the |
| * unit conversion terms are often defined with a value in base 10. That value is considered exact |
| * by definition, but IEEE 754 has no exact representation of decimal fraction digits. |
| * |
| * <p>It is caller's responsibility to skip this method call when {@code numerator} = 0. |
| * This method does not perform this check because it is usually already done by the caller.</p> |
| */ |
| static LinearConverter offset(final double numerator, final double denominator) { |
| return new LinearConverter(denominator, numerator, denominator); |
| } |
| |
| /** |
| * Raises the given converter to the given power. This method assumes that the given converter |
| * {@linkplain #isLinear() is linear} (this is not verified) and takes only the scale factor; |
| * the offset (if any) is ignored. |
| * |
| * <p>It is caller's responsibility to skip this method call when {@code n} = 1. |
| * This method does not perform this check because it is usually already done (indirectly) by the caller.</p> |
| * |
| * @param converter the converter to raise to the given power. |
| * @param n the exponent. |
| * @param root {@code true} for raising to 1/n instead of n. |
| * @return the converter raised to the given power. |
| */ |
| static LinearConverter pow(final UnitConverter converter, final int n, final boolean root) { |
| double numerator, denominator; |
| if (converter instanceof LinearConverter) { |
| final LinearConverter lc = (LinearConverter) converter; |
| numerator = lc.scale; |
| denominator = lc.divisor; |
| } else { |
| // Subtraction by convert(0) is a paranoiac safety. |
| numerator = converter.convert(1d) - converter.convert(0d); |
| denominator = 1; |
| } |
| if (root) { |
| switch (n) { |
| case 1: break; |
| case 2: numerator = Math.sqrt(numerator); |
| denominator = Math.sqrt(denominator); |
| break; |
| case 3: numerator = Math.cbrt(numerator); |
| denominator = Math.cbrt(denominator); |
| break; |
| default: final double r = 1.0 / n; |
| numerator = Math.pow(numerator, r); |
| denominator = Math.pow(denominator, r); |
| break; |
| } |
| } else { |
| numerator = (numerator == 10) ? MathFunctions.pow10(n) : Math.pow(numerator, n); |
| denominator = (denominator == 10) ? MathFunctions.pow10(n) : Math.pow(denominator, n); |
| } |
| return scale(numerator, denominator); |
| } |
| |
| /** |
| * Indicates if this converter is linear. |
| * JSR-385 defines a converter as linear if: |
| * |
| * <ul> |
| * <li>{@code convert(u + v) == convert(u) + convert(v)}</li> |
| * <li>{@code convert(r * u) == r * convert(u)}</li> |
| * </ul> |
| * |
| * Note that this definition allows scale factors but does not allow offsets. |
| * Consequently, this is a different definition of "linear" than this class and the rest of Apache SIS. |
| */ |
| @Override |
| public boolean isLinear() { |
| return offset == 0; |
| } |
| |
| /** |
| * Returns {@code true} if the effective scale factor is 1 and the offset is zero. |
| */ |
| @Override |
| public boolean isIdentity() { |
| return scale == divisor && offset == 0; |
| } |
| |
| /** |
| * Returns {@code true} if this converter is close to an identity converter. |
| */ |
| final boolean almostIdentity() { |
| return epsilonEquals(scale, divisor) && epsilonEquals(offset, 0); |
| } |
| |
| /** |
| * Returns the inverse of this unit converter. |
| * Given that the formula applied by this converter is: |
| * |
| * <pre class="math"> |
| * y = (x⋅scale + offset) ∕ divisor</pre> |
| * |
| * the inverse formula is: |
| * |
| * <pre class="math"> |
| * x = (y⋅divisor - offset) ∕ scale</pre> |
| */ |
| @Override |
| public synchronized UnitConverter inverse() { |
| if (inverse == null) { |
| inverse = isIdentity() ? this : new LinearConverter(divisor, -offset, scale); |
| inverse.inverse = this; |
| } |
| return inverse; |
| } |
| |
| /** |
| * Returns the coefficient of this linear conversion. |
| * Coefficients are offset and scale, in that order. |
| */ |
| @Override |
| @SuppressWarnings("fallthrough") |
| final Number[] coefficients() { |
| final Number[] c = new Number[(scale != divisor) ? 2 : (offset != 0) ? 1 : 0]; |
| switch (c.length) { |
| case 2: c[1] = ratio(scale, divisor); |
| case 1: c[0] = ratio(offset, divisor); |
| case 0: break; |
| } |
| return c; |
| } |
| |
| /** |
| * Returns the given ratio as a {@link Fraction} if possible, or as a {@link Double} otherwise. |
| * The use of {@link Fraction} allows the {@link org.apache.sis.referencing.operation.matrix} |
| * package to perform more accurate calculations. |
| */ |
| private static Number ratio(final double value, final double divisor) { |
| if (value != 0) { |
| final int numerator = (int) value; |
| if (numerator == value) { |
| final int denominator = (int) divisor; |
| if (denominator == divisor) { |
| return (denominator == 1) ? numerator : new Fraction(numerator, denominator); |
| } |
| } |
| } |
| return value / divisor; |
| } |
| |
| /** |
| * Applies the linear conversion on the given IEEE 754 floating-point value. |
| */ |
| @Override |
| public double convert(final double value) { |
| return Math.fma(value, scale, offset) / divisor; |
| } |
| |
| /** |
| * Applies the linear conversion on the given value. This method uses {@link BigDecimal} arithmetic if |
| * the given value is an instance of {@code BigDecimal}, or double-double arithmetic if the value is an |
| * instance of {@link DoubleDouble}, or IEEE 754 floating-point arithmetic otherwise. |
| */ |
| @Override |
| public Number convert(Number value) { |
| ArgumentChecks.ensureNonNull("value", value); |
| if (!isIdentity()) { |
| if (value instanceof DoubleDouble) { |
| var dd = (DoubleDouble) value; |
| return dd.multiply(scale, true).add(offset, true).divide(divisor, true); |
| } |
| if (value instanceof BigInteger) { |
| value = new BigDecimal((BigInteger) value); |
| } |
| if (value instanceof BigDecimal) { |
| BigDecimal scale10 = this.scale10; |
| BigDecimal offset10 = this.offset10; |
| if (scale10 == null || offset10 == null) { |
| final BigDecimal divisor = BigDecimal.valueOf(this.divisor); |
| scale10 = BigDecimal.valueOf(scale) .divide(divisor); |
| offset10 = BigDecimal.valueOf(offset).divide(divisor); |
| this.scale10 = scale10; // Volatile fields |
| this.offset10 = offset10; |
| } |
| value = ((BigDecimal) value).multiply(scale10).add(offset10); |
| } else { |
| value = convert(doubleValue(value)); |
| } |
| } |
| return value; |
| } |
| |
| /** |
| * Returns the derivative of the conversion at the given value. |
| * For a linear converter, the derivative is the same everywhere. |
| */ |
| @Override |
| public double derivative(double value) { |
| return scale / divisor; |
| } |
| |
| /** |
| * Concatenates this converter with another converter. The resulting converter is equivalent to first converting |
| * by the specified converter (right converter), and then converting by this converter (left converter). In the |
| * following equations, the 1 subscript is for the specified converter and the 2 subscript is for this converter: |
| * |
| * <pre class="math"> |
| * t = (x⋅scale₁ + offset₁) ∕ divisor₁ |
| * y = (t⋅scale₂ + offset₂) ∕ divisor₂</pre> |
| * |
| * We rewrite as: |
| * |
| * <pre class="math"> |
| * y = (x⋅scale₁⋅scale₂ + offset₁⋅scale₂ + divisor₁⋅offset₂) ∕ (divisor₁⋅divisor₂)</pre> |
| */ |
| @Override |
| public UnitConverter concatenate(final UnitConverter converter) { |
| ArgumentChecks.ensureNonNull("converter", converter); |
| if (converter.isIdentity()) { |
| return this; |
| } |
| if (isIdentity()) { |
| return converter; |
| } |
| double otherScale, otherOffset, otherDivisor; |
| if (converter instanceof LinearConverter) { |
| final LinearConverter lc = (LinearConverter) converter; |
| otherScale = lc.scale; |
| otherOffset = lc.offset; |
| otherDivisor = lc.divisor; |
| } else if (converter.isLinear()) { |
| /* |
| * Fallback for foreigner implementations. Note that `otherOffset` should be restricted to zero |
| * according JSR-385 definition of `isLinear()`, but let be safe; maybe we are not the only one |
| * to have a different interpretation about the meaning of "linear". |
| */ |
| otherOffset = converter.convert(0.0); |
| otherScale = converter.convert(1.0) - otherOffset; |
| otherDivisor = 1; |
| } else { |
| return new ConcatenatedConverter(converter, this); |
| } |
| otherScale *= scale; |
| otherOffset = otherOffset * scale + otherDivisor * offset; |
| otherDivisor *= divisor; |
| /* |
| * Following loop is a little bit similar to simplifying a fraction, but checking only for the |
| * powers of 10 since unit conversions are often such values. Algorithm is not very efficient, |
| * but the loop should not be executed often. |
| */ |
| if (otherScale != 0 || otherOffset != 0 || otherDivisor != 0) { |
| double cf, f = 1; |
| do { |
| cf = f; |
| f *= 10; |
| } while (otherScale % f == 0 && otherOffset % f == 0 && otherDivisor % f == 0); |
| otherScale /= cf; |
| otherOffset /= cf; |
| otherDivisor /= cf; |
| } |
| if (otherOffset == 0 && otherScale == otherDivisor) { |
| return IdentityConverter.INSTANCE; |
| } |
| return new LinearConverter(otherScale, otherOffset, otherDivisor); |
| } |
| |
| /** |
| * Returns a hash code value for this unit converter. |
| */ |
| @Override |
| public int hashCode() { |
| return Long.hashCode(Double.doubleToLongBits(scale) |
| + 31 * (Double.doubleToLongBits(offset) |
| + 37 * Double.doubleToLongBits(divisor))); |
| } |
| |
| /** |
| * Compares this converter with the given object for equality. |
| */ |
| @Override |
| public boolean equals(final Object other) { |
| if (other instanceof LinearConverter) { |
| final LinearConverter o = (LinearConverter) other; |
| return Numerics.equals(scale, o.scale) && |
| Numerics.equals(offset, o.offset) && |
| Numerics.equals(divisor, o.divisor); |
| } |
| if (other instanceof IdentityConverter) { |
| return isIdentity(); |
| } |
| return false; |
| } |
| |
| /** |
| * Compares this converter with the given object for equality, optionally ignoring rounding errors. |
| */ |
| @Override |
| public boolean equals(final Object other, final ComparisonMode mode) { |
| if (mode.isApproximate()) { |
| if (other instanceof LinearConverter) { |
| return equivalent((LinearConverter) other); |
| } else if (other instanceof IdentityConverter) { |
| return almostIdentity(); |
| } else { |
| return false; |
| } |
| } else { |
| return equals(other); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the given converter perform the same conversion than this converter, |
| * except for rounding errors. |
| */ |
| final boolean equivalent(final LinearConverter other) { |
| return epsilonEquals(scale * other.divisor, other.scale * divisor) && |
| epsilonEquals(offset * other.divisor, other.offset * divisor); |
| } |
| |
| /** |
| * Returns a string representation of this converter for debugging purpose. |
| * This string representation may change in any future SIS release. |
| * Current format is of the form "y = scale⋅x + offset". |
| */ |
| @Override |
| public String toString() { |
| final StringBuilder buffer = new StringBuilder().append("y = "); |
| if (offset != 0) { |
| buffer.append('('); |
| } |
| if (scale != 1) { |
| StringBuilders.trimFractionalPart(buffer.append(scale)); |
| buffer.append(AbstractUnit.MULTIPLY); |
| } |
| buffer.append('x'); |
| if (offset != 0) { |
| StringBuilders.trimFractionalPart(buffer.append(" + ").append(offset)); |
| buffer.append(')'); |
| } |
| if (divisor != 1) { |
| StringBuilders.trimFractionalPart(buffer.append(AbstractUnit.DIVIDE).append(divisor)); |
| } |
| return buffer.toString(); |
| } |
| } |