| /* |
| * 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.matrix; |
| |
| import java.util.Arrays; |
| import java.util.Objects; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.referencing.cs.AxisDirection; |
| import org.opengis.referencing.cs.CoordinateSystem; // For javadoc |
| import org.opengis.referencing.operation.Matrix; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.geometry.MismatchedDimensionException; |
| import org.apache.sis.util.Static; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.StringBuilders; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.math.DecimalFunctions; |
| import org.apache.sis.internal.util.Numerics; |
| import org.apache.sis.internal.util.DoubleDouble; |
| import org.apache.sis.internal.referencing.AxisDirections; |
| import org.apache.sis.internal.referencing.Resources; |
| import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix; |
| |
| |
| /** |
| * {@link Matrix} factory methods and utilities. |
| * This class provides the following methods: |
| * |
| * <ul> |
| * <li>Creating new matrices of arbitrary size: |
| * {@link #createIdentity createIdentity}, |
| * {@link #createDiagonal createDiagonal}, |
| * {@link #createZero createZero}, |
| * {@link #create create}, |
| * {@link #copy copy}. |
| * </li> |
| * <li>Creating new matrices for coordinate operation steps: |
| * {@link #createTransform(Envelope, AxisDirection[], Envelope, AxisDirection[]) createTransform}, |
| * {@link #createDimensionSelect createDimensionSelect}, |
| * {@link #createPassThrough createPassThrough}. |
| * </li> |
| * <li>Information: |
| * {@link #isAffine isAffine}, |
| * {@link #isIdentity isIdentity}, |
| * {@link #equals(Matrix, Matrix, double, boolean) equals}, |
| * {@link #toString(Matrix) toString}. |
| * </li> |
| * <li>Miscellaneous: |
| * {@link #multiply multiply}, |
| * {@link #inverse inverse}, |
| * {@link #unmodifiable unmodifiable}, |
| * </li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @version 1.0 |
| * |
| * @see org.apache.sis.parameter.TensorParameters |
| * |
| * @since 0.4 |
| * @module |
| */ |
| public final class Matrices extends Static { |
| /** |
| * Number of spaces to put between columns formatted by {@link #toString(Matrix)}. |
| */ |
| private static final int SPACING = 2; |
| |
| /** |
| * Do not allows instantiation of this class. |
| */ |
| private Matrices() { |
| } |
| |
| /** |
| * Creates a square identity matrix of size {@code size} × {@code size}. |
| * Elements on the diagonal (<var>j</var> == <var>i</var>) are set to 1. |
| * |
| * <div class="note"><b>Implementation note:</b> |
| * For sizes between {@value org.apache.sis.referencing.operation.matrix.Matrix1#SIZE} and |
| * {@value org.apache.sis.referencing.operation.matrix.Matrix4#SIZE} inclusive, the matrix |
| * is guaranteed to be an instance of one of {@link Matrix1} … {@link Matrix4} subtypes.</div> |
| * |
| * @param size numbers of row and columns. For an affine transform matrix, this is the number of |
| * {@linkplain MathTransform#getSourceDimensions() source} and |
| * {@linkplain MathTransform#getTargetDimensions() target} dimensions + 1. |
| * @return an identity matrix of the given size. |
| */ |
| public static MatrixSIS createIdentity(final int size) { |
| switch (size) { |
| case 1: return new Matrix1(); |
| case 2: return new Matrix2(); |
| case 3: return new Matrix3(); |
| case 4: return new Matrix4(); |
| default: return new GeneralMatrix(size, size, true, 1); |
| } |
| } |
| |
| /** |
| * Creates a matrix of size {@code numRow} × {@code numCol}. |
| * Elements on the diagonal (<var>j</var> == <var>i</var>) are set to 1. |
| * The result is an identity matrix if {@code numRow} = {@code numCol}. |
| * |
| * <div class="note"><b>Implementation note:</b> |
| * For {@code numRow} == {@code numCol} with a value between |
| * {@value org.apache.sis.referencing.operation.matrix.Matrix1#SIZE} and |
| * {@value org.apache.sis.referencing.operation.matrix.Matrix4#SIZE} inclusive, the matrix |
| * is guaranteed to be an instance of one of {@link Matrix1} … {@link Matrix4} subtypes.</div> |
| * |
| * @param numRow for a math transform, this is the number of {@linkplain MathTransform#getTargetDimensions() target dimensions} + 1. |
| * @param numCol for a math transform, this is the number of {@linkplain MathTransform#getSourceDimensions() source dimensions} + 1. |
| * @return an identity matrix of the given size. |
| */ |
| public static MatrixSIS createDiagonal(final int numRow, final int numCol) { |
| if (numRow == numCol) { |
| return createIdentity(numRow); |
| } else { |
| return new NonSquareMatrix(numRow, numCol, true, 1); |
| } |
| } |
| |
| /** |
| * Creates a matrix of size {@code numRow} × {@code numCol} filled with zero values. |
| * This constructor is convenient when the caller wants to initialize the matrix elements himself. |
| * |
| * <div class="note"><b>Implementation note:</b> |
| * For {@code numRow} == {@code numCol} with a value between |
| * {@value org.apache.sis.referencing.operation.matrix.Matrix1#SIZE} and |
| * {@value org.apache.sis.referencing.operation.matrix.Matrix4#SIZE} inclusive, the matrix |
| * is guaranteed to be an instance of one of {@link Matrix1} … {@link Matrix4} subtypes.</div> |
| * |
| * @param numRow for a math transform, this is the number of {@linkplain MathTransform#getTargetDimensions() target dimensions} + 1. |
| * @param numCol for a math transform, this is the number of {@linkplain MathTransform#getSourceDimensions() source dimensions} + 1. |
| * @return a matrix of the given size with only zero values. |
| */ |
| public static MatrixSIS createZero(final int numRow, final int numCol) { |
| if (numRow == numCol) switch (numRow) { |
| case 1: return new Matrix1(false); |
| case 2: return new Matrix2(false); |
| case 3: return new Matrix3(false); |
| case 4: return new Matrix4(false); |
| default: return new GeneralMatrix(numRow, numCol, false, 1); |
| } |
| return new NonSquareMatrix(numRow, numCol, false, 1); |
| } |
| |
| /** |
| * Creates a matrix filled with zero values, using double-double precision if {@code precision} is {@code true}. |
| */ |
| static MatrixSIS createZero(final int numRow, final int numCol, final boolean precision) { |
| return precision ? GeneralMatrix.createExtendedPrecision(numRow, numCol, false) : createZero(numRow, numCol); |
| } |
| |
| /** |
| * Creates a matrix of size {@code numRow} × {@code numCol} initialized to the given elements. |
| * The elements array size must be equals to {@code numRow*numCol}. Column indices vary fastest. |
| * |
| * <div class="note"><b>Implementation note:</b> |
| * For {@code numRow} == {@code numCol} with a value between |
| * {@value org.apache.sis.referencing.operation.matrix.Matrix1#SIZE} and |
| * {@value org.apache.sis.referencing.operation.matrix.Matrix4#SIZE} inclusive, the matrix |
| * is guaranteed to be an instance of one of {@link Matrix1} … {@link Matrix4} subtypes.</div> |
| * |
| * @param numRow number of rows. |
| * @param numCol number of columns. |
| * @param elements the matrix elements in a row-major array. Column indices vary fastest. |
| * @return a matrix initialized to the given elements. |
| * |
| * @see MatrixSIS#setElements(double[]) |
| */ |
| public static MatrixSIS create(final int numRow, final int numCol, final double[] elements) { |
| if (numRow == numCol) switch (numRow) { |
| case 1: return new Matrix1(elements); |
| case 2: return new Matrix2(elements); |
| case 3: return new Matrix3(elements); |
| case 4: return new Matrix4(elements); |
| default: return new GeneralMatrix(numRow, numCol, elements); |
| } |
| return new NonSquareMatrix(numRow, numCol, elements); |
| } |
| |
| /** |
| * Creates a matrix of size {@code numRow} × {@code numCol} initialized to the given numbers. |
| * The elements array size must be equals to {@code numRow*numCol}. Column indices vary fastest. |
| * |
| * @param numRow number of rows. |
| * @param numCol number of columns. |
| * @param elements the matrix elements in a row-major array. Column indices vary fastest. |
| * @return a matrix initialized to the given elements. |
| */ |
| public static MatrixSIS create(final int numRow, final int numCol, final Number[] elements) { |
| ArgumentChecks.ensureNonNull("elements", elements); |
| /* |
| * Below is an intantionally undocumented feature. We use those sentinel values as a way to create |
| * matrices with extended precision without exposing our double-double arithmetic in public API. |
| */ |
| if (elements == ExtendedPrecisionMatrix.IDENTITY || elements == ExtendedPrecisionMatrix.ZERO) { |
| return GeneralMatrix.createExtendedPrecision(numRow, numCol, elements == ExtendedPrecisionMatrix.IDENTITY); |
| } |
| final GeneralMatrix matrix = GeneralMatrix.createExtendedPrecision(numRow, numCol, false); |
| if (matrix.setElements(elements)) { |
| /* |
| * At least one org.apache.sis.internal.util.DoubleDouble instance has been found, |
| * in which case the matrix uses double-double arithmetic. This case is the main |
| * purpose of this method. |
| */ |
| return matrix; |
| } |
| return create(numRow, numCol, matrix.getElements()); |
| } |
| |
| /** |
| * Implementation of {@code createTransform(…)} public methods expecting envelopes and/or axis directions. |
| * Argument validity shall be verified by the caller. |
| * |
| * @param useEnvelopes {@code true} if source and destination envelopes shall be taken in account. |
| * If {@code false}, then source and destination envelopes will be ignored and can be null. |
| */ |
| @SuppressWarnings("null") |
| private static MatrixSIS createTransform(final Envelope srcEnvelope, final AxisDirection[] srcAxes, |
| final Envelope dstEnvelope, final AxisDirection[] dstAxes, |
| final boolean useEnvelopes) |
| { |
| final DirectPosition dstCorner, srcCorner, srcOppositeCorner; |
| if (useEnvelopes) { |
| dstCorner = dstEnvelope.getLowerCorner(); |
| srcCorner = srcEnvelope.getLowerCorner(); |
| srcOppositeCorner = srcEnvelope.getUpperCorner(); |
| } else { |
| dstCorner = srcCorner = srcOppositeCorner = null; |
| } |
| /* |
| * Unconditionally create extended precision matrix even if standard precision would be |
| * enough because callers in other package may perform additional arithmetic operations |
| * on it (for example org.apache.sis.referencing.cs.CoordinateSystems.swapAndScaleAxes). |
| */ |
| final MatrixSIS matrix = new GeneralMatrix(dstAxes.length+1, srcAxes.length+1, false, 2); |
| /* |
| * Maps source axes to destination axes. If no axis is moved (for example if the user |
| * want to transform (NORTH,EAST) to (SOUTH,EAST)), then source and destination index |
| * will be equal. If some axes are moved (for example if the user want to transform |
| * (NORTH,EAST) to (EAST,NORTH)), then coordinates at index {@code srcIndex} will have |
| * to be moved at index {@code dstIndex}. |
| */ |
| for (int dstIndex = 0; dstIndex < dstAxes.length; dstIndex++) { |
| boolean hasFound = false; |
| final AxisDirection dstDir = dstAxes[dstIndex]; |
| final AxisDirection search = AxisDirections.absolute(dstDir); |
| for (int srcIndex = 0; srcIndex < srcAxes.length; srcIndex++) { |
| final AxisDirection srcDir = srcAxes[srcIndex]; |
| if (search.equals(AxisDirections.absolute(srcDir))) { |
| if (hasFound) { |
| throw new IllegalArgumentException(Resources.format( |
| Resources.Keys.ColinearAxisDirections_2, srcDir, dstDir)); |
| } |
| hasFound = true; |
| /* |
| * Set the matrix elements. Some matrix elements will never be set. |
| * They will be left to zero, which is their desired value. |
| */ |
| final boolean same = srcDir.equals(dstDir); |
| if (useEnvelopes) { |
| /* |
| * See the comment in transform(Envelope, Envelope) for an explanation about why |
| * we use the lower/upper corners instead than getMinimum()/getMaximum() methods. |
| */ |
| final DoubleDouble scale = new DoubleDouble(same ? +1d : -1d); |
| scale.multiplyGuessError(dstEnvelope.getSpan(dstIndex)); |
| scale.divideGuessError (srcEnvelope.getSpan(srcIndex)); |
| |
| final DoubleDouble translate = new DoubleDouble(scale); |
| translate.multiplyGuessError((same ? srcCorner : srcOppositeCorner).getOrdinate(srcIndex)); |
| translate.negate(); |
| translate.addGuessError(dstCorner.getOrdinate(dstIndex)); |
| |
| matrix.setNumber(dstIndex, srcIndex, scale); |
| matrix.setNumber(dstIndex, srcAxes.length, translate); |
| } else { |
| matrix.setElement(dstIndex, srcIndex, same ? +1 : -1); |
| } |
| } |
| } |
| if (!hasFound) { |
| throw new IllegalArgumentException(Resources.format( |
| Resources.Keys.CanNotMapAxisToDirection_1, dstAxes[dstIndex])); |
| } |
| } |
| matrix.setElement(dstAxes.length, srcAxes.length, 1); |
| return matrix; |
| } |
| |
| /** |
| * Creates a transform matrix mapping the given source envelope to the given destination envelope. |
| * The given envelopes can have any dimensions, which are handled as below: |
| * |
| * <ul> |
| * <li>If the two envelopes have the same {@linkplain Envelope#getDimension() dimension}, |
| * then the transform is {@linkplain #isAffine(Matrix) affine}.</li> |
| * <li>If the destination envelope has less dimensions than the source envelope, |
| * then trailing dimensions are silently dropped.</li> |
| * <li>If the target envelope has more dimensions than the source envelope, |
| * then the transform will append trailing coordinates with the 0 value.</li> |
| * </ul> |
| * |
| * This method ignores the {@linkplain Envelope#getCoordinateReferenceSystem() envelope CRS}, which may be null. |
| * Actually this method is used more often for {@linkplain org.opengis.coverage.grid.GridEnvelope grid envelopes} |
| * (which have no CRS) than geodetic envelopes. |
| * |
| * <h4>Spanning the anti-meridian of a Geographic CRS</h4> |
| * If the given envelopes cross the date line, then this method requires their {@code getSpan(int)} method |
| * to behave as documented in the {@link org.apache.sis.geometry.AbstractEnvelope#getSpan(int)} javadoc. |
| * Furthermore the matrix created by this method will produce expected results only for source or destination |
| * points before the date line, since the wrap around operation can not be represented by an affine transform. |
| * |
| * <h4>Example</h4> |
| * Given a source envelope of size 100 × 200 (the units do not matter for this method) and a destination |
| * envelope of size 300 × 500, and given {@linkplain Envelope#getLowerCorner() lower corner} translation |
| * from (-20, -40) to (-10, -25), then the following method call: |
| * |
| * {@preformat java |
| * matrix = Matrices.createTransform( |
| * new Envelope2D(null, -20, -40, 100, 200), |
| * new Envelope2D(null, -10, -25, 300, 500)); |
| * } |
| * |
| * will return the following square matrix. The transform of the lower corner is given as an example: |
| * |
| * {@preformat math |
| * ┌ ┐ ┌ ┐ ┌ ┐ |
| * │ -10 │ │ 3.0 0 50 │ │ -20 │ // 3.0 is the scale factor from width of 100 to 300 |
| * │ -25 │ = │ 0 2.5 75 │ × │ -40 │ // 2.5 is the scale factor from height of 200 to 500 |
| * │ 1 │ │ 0 0 1 │ │ 1 │ |
| * └ ┘ └ ┘ └ ┘ |
| * } |
| * |
| * @param srcEnvelope the source envelope. |
| * @param dstEnvelope the destination envelope. |
| * @return the transform from the given source envelope to target envelope. |
| * |
| * @see #createTransform(AxisDirection[], AxisDirection[]) |
| * @see #createTransform(Envelope, AxisDirection[], Envelope, AxisDirection[]) |
| * @see org.apache.sis.referencing.cs.CoordinateSystems#swapAndScaleAxes(CoordinateSystem, CoordinateSystem) |
| */ |
| public static MatrixSIS createTransform(final Envelope srcEnvelope, final Envelope dstEnvelope) { |
| ArgumentChecks.ensureNonNull("srcEnvelope", srcEnvelope); |
| ArgumentChecks.ensureNonNull("dstEnvelope", dstEnvelope); |
| /* |
| * Following code is a simplified version of above createTransform(Envelope, AxisDirection[], ...) method. |
| * We need to make sure that those two methods are consistent and compute the matrix values in the same way. |
| */ |
| final int srcDim = srcEnvelope.getDimension(); |
| final int dstDim = dstEnvelope.getDimension(); |
| final DirectPosition srcCorner = srcEnvelope.getLowerCorner(); |
| final DirectPosition dstCorner = dstEnvelope.getLowerCorner(); |
| final MatrixSIS matrix = createZero(dstDim+1, srcDim+1); |
| for (int i = Math.min(srcDim, dstDim); --i >= 0;) { |
| /* |
| * Note on envelope spanning over the anti-meridian: the GeoAPI javadoc does not mandate the |
| * precise behavior of getSpan(int) in such situation. In the particular case of Apache SIS |
| * implementations, the envelope will compute the span correctly (taking in account the wrap |
| * around behavior). For non-SIS implementations, we can not know. |
| * |
| * For the translation term, we really need the lower corner, NOT envelope.getMinimum(i), |
| * because we need the starting point, which is not the minimal value when spanning over |
| * the anti-meridian. |
| */ |
| final double scale = dstEnvelope.getSpan(i) / srcEnvelope.getSpan(i); |
| final double translate = dstCorner.getOrdinate(i) - srcCorner.getOrdinate(i)*scale; |
| matrix.setElement(i, i, scale); |
| matrix.setElement(i, srcDim, translate); |
| } |
| matrix.setElement(dstDim, srcDim, 1); |
| return matrix; |
| } |
| |
| /** |
| * Creates a transform matrix changing axis order and/or direction. For example the transform may convert |
| * (<i>northing</i>, <i>westing</i>) coordinates into (<i>easting</i>, <i>northing</i>) coordinates. |
| * This method tries to associate each {@code dstAxes} direction to either an equals {@code srcAxis} |
| * direction, or to an opposite {@code srcAxis} direction. |
| * |
| * <ul> |
| * <li>If some {@code srcAxes} directions can not be mapped to {@code dstAxes} directions, then the transform |
| * will silently drops the coordinates associated to those extra source axis directions.</li> |
| * <li>If some {@code dstAxes} directions can not be mapped to {@code srcAxes} directions, |
| * then an exception will be thrown.</li> |
| * </ul> |
| * |
| * <div class="note"><b>Example:</b> |
| * it is legal to transform from (<i>easting</i>, <i>northing</i>, <i>up</i>) to |
| * (<i>easting</i>, <i>northing</i>) — this is the first above case — but illegal |
| * to transform (<i>easting</i>, <i>northing</i>) to (<i>easting</i>, <i>up</i>).</div> |
| * |
| * <h4>Example</h4> |
| * The following method call: |
| * |
| * {@preformat java |
| * matrix = Matrices.createTransform( |
| * new AxisDirection[] {AxisDirection.NORTH, AxisDirection.WEST}, |
| * new AxisDirection[] {AxisDirection.EAST, AxisDirection.NORTH}); |
| * } |
| * |
| * will return the following square matrix, which can be used in coordinate conversions as below: |
| * |
| * {@preformat math |
| * ┌ ┐ ┌ ┐ ┌ ┐ |
| * │ +x │ │ 0 -1 0 │ │ y │ |
| * │ y │ = │ 1 0 0 │ × │ -x │ |
| * │ 1 │ │ 0 0 1 │ │ 1 │ |
| * └ ┘ └ ┘ └ ┘ |
| * } |
| * |
| * @param srcAxes the ordered sequence of axis directions for source coordinate system. |
| * @param dstAxes the ordered sequence of axis directions for destination coordinate system. |
| * @return the transform from the given source axis directions to the given target axis directions. |
| * @throws IllegalArgumentException if {@code dstAxes} contains at least one axis not found in {@code srcAxes}, |
| * or if some colinear axes were found. |
| * |
| * @see #createTransform(Envelope, Envelope) |
| * @see #createTransform(Envelope, AxisDirection[], Envelope, AxisDirection[]) |
| * @see org.apache.sis.referencing.cs.CoordinateSystems#swapAndScaleAxes(CoordinateSystem, CoordinateSystem) |
| */ |
| public static MatrixSIS createTransform(final AxisDirection[] srcAxes, final AxisDirection[] dstAxes) { |
| ArgumentChecks.ensureNonNull("srcAxes", srcAxes); |
| ArgumentChecks.ensureNonNull("dstAxes", dstAxes); |
| if (Arrays.equals(srcAxes, dstAxes)) { |
| /* |
| * createTransform(…) may fail if the arrays contain two axes with the same direction, for example |
| * AxisDirection.OTHER. This check prevents that failure for the common case of an identity transform. |
| */ |
| return Matrices.createIdentity(srcAxes.length + 1); |
| } |
| return createTransform(null, srcAxes, null, dstAxes, false); |
| } |
| |
| /** |
| * Creates a transform matrix mapping the given source envelope to the given destination envelope, |
| * combined with changes in axis order and/or direction. |
| * Invoking this method is equivalent to concatenating the following matrix transforms: |
| * |
| * <ul> |
| * <li><code>{@linkplain #createTransform(Envelope, Envelope) createTransform}(srcEnvelope, dstEnvelope)</code></li> |
| * <li><code>{@linkplain #createTransform(AxisDirection[], AxisDirection[]) createTransform}(srcAxes, dstAxes)</code></li> |
| * </ul> |
| * |
| * This method ignores the {@linkplain Envelope#getCoordinateReferenceSystem() envelope CRS}, which may be null. |
| * Actually this method is used more often for {@linkplain org.opengis.coverage.grid.GridEnvelope grid envelopes} |
| * (which have no CRS) than geodetic envelopes. |
| * |
| * <h4>Spanning the anti-meridian of a Geographic CRS</h4> |
| * If the given envelopes cross the date line, then this method requires their {@code getSpan(int)} method |
| * to behave as documented in the {@link org.apache.sis.geometry.AbstractEnvelope#getSpan(int)} javadoc. |
| * Furthermore the matrix created by this method will produce expected results only for source or destination |
| * points on one side of the date line (depending on whether axis direction is reversed), since the wrap around |
| * operation can not be represented by an affine transform. |
| * |
| * <div class="note"><b>Example:</b> |
| * combining the examples documented in the above {@code createTransform(…)} methods, the following method call: |
| * |
| * {@preformat java |
| * matrix = Matrices.createTransform( |
| * new Envelope2D(null, -40, +20, 200, 100), new AxisDirection[] {AxisDirection.NORTH, AxisDirection.WEST}, |
| * new Envelope2D(null, -10, -25, 300, 500), new AxisDirection[] {AxisDirection.EAST, AxisDirection.NORTH}); |
| * } |
| * |
| * will return the following square matrix. The transform of a corner is given as an example. |
| * Note that the input coordinate values are swapped because of the (<i>North</i>, <i>West</i>) axis directions, |
| * and the lower-left corner of the destination envelope is the lower-<em>right</em> corner of the source envelope |
| * because of the opposite axis direction. |
| * |
| * {@preformat math |
| * ┌ ┐ ┌ ┐ ┌ ┐ |
| * │ -10 │ │ 0 -3.0 350 │ │ -40 │ |
| * │ -25 │ = │ 2.5 0 75 │ × │ 120 │ // 120 is the westernmost source coordinate: (x=20) + (width=100) |
| * │ 1 │ │ 0 0 1 │ │ 1 │ |
| * └ ┘ └ ┘ └ ┘ |
| * } |
| * </div> |
| * |
| * @param srcEnvelope the source envelope. |
| * @param srcAxes the ordered sequence of axis directions for source coordinate system. |
| * @param dstEnvelope the destination envelope. |
| * @param dstAxes the ordered sequence of axis directions for destination coordinate system. |
| * @return the transform from the given source envelope and axis directions |
| * to the given envelope and target axis directions. |
| * @throws MismatchedDimensionException if an envelope {@linkplain Envelope#getDimension() dimension} does not |
| * match the length of the axis directions sequence. |
| * @throws IllegalArgumentException if {@code dstAxes} contains at least one axis not found in {@code srcAxes}, |
| * or if some colinear axes were found. |
| * |
| * @see #createTransform(Envelope, Envelope) |
| * @see #createTransform(AxisDirection[], AxisDirection[]) |
| * @see org.apache.sis.referencing.cs.CoordinateSystems#swapAndScaleAxes(CoordinateSystem, CoordinateSystem) |
| */ |
| public static MatrixSIS createTransform(final Envelope srcEnvelope, final AxisDirection[] srcAxes, |
| final Envelope dstEnvelope, final AxisDirection[] dstAxes) |
| { |
| ArgumentChecks.ensureNonNull("srcEnvelope", srcEnvelope); |
| ArgumentChecks.ensureNonNull("dstEnvelope", dstEnvelope); |
| ArgumentChecks.ensureDimensionMatches("srcEnvelope", srcAxes.length, srcEnvelope); |
| ArgumentChecks.ensureDimensionMatches("dstEnvelope", dstAxes.length, dstEnvelope); |
| return createTransform(srcEnvelope, srcAxes, dstEnvelope, dstAxes, true); |
| } |
| |
| /** |
| * Creates a matrix for a transform that keep only a subset of source coordinate values. |
| * The matrix size will be ({@code selectedDimensions.length} + 1) × ({@code sourceDimensions} + 1). |
| * The matrix will contain only zero elements, except for the following cells which will contain 1: |
| * |
| * <ul> |
| * <li>The last column in the last row.</li> |
| * <li>For any row <var>j</var> other than the last row, the column {@code selectedDimensions[j]}.</li> |
| * </ul> |
| * |
| * <div class="note"><b>Example:</b> |
| * given (<var>x</var>,<var>y</var>,<var>z</var>,<var>t</var>) coordinate values, if one wants to keep |
| * (<var>y</var>,<var>x</var>,<var>t</var>) coordinates (note the <var>x</var> ↔ <var>y</var> swapping) |
| * and discard the <var>z</var> values, then the indices of source coordinates to select are 1 for <var>y</var>, |
| * 0 for <var>x</var> and 3 for <var>t</var>. One can use the following method call: |
| * |
| * {@preformat java |
| * matrix = Matrices.createDimensionSelect(4, new int[] {1, 0, 3}); |
| * } |
| * |
| * The above method call will create the following 4×5 matrix, |
| * which can be used for converting coordinates as below: |
| * |
| * {@preformat math |
| * ┌ ┐ ┌ ┐ ┌ ┐ |
| * │ y │ │ 0 1 0 0 0 │ │ x │ |
| * │ x │ │ 1 0 0 0 0 │ │ y │ |
| * │ t │ = │ 0 0 0 1 0 │ × │ z │ |
| * │ 1 │ │ 0 0 0 0 1 │ │ t │ |
| * └ ┘ └ ┘ │ 1 │ |
| * └ ┘ |
| * } |
| * </div> |
| * |
| * The inverse of the matrix created by this method will put {@link Double#NaN} values in the extra dimensions. |
| * Other dimensions will work as expected. |
| * |
| * @param sourceDimensions the number of dimensions in source coordinates. |
| * @param selectedDimensions the 0-based indices of source coordinate values to keep. |
| * The length of this array will be the number of dimensions in target coordinates. |
| * @return an affine transform matrix keeping only the given source dimensions, and discarding all others. |
| * @throws IllegalArgumentException if a value of {@code selectedDimensions} is lower than 0 |
| * or not smaller than {@code sourceDimensions}. |
| * |
| * @see org.apache.sis.referencing.operation.transform.TransformSeparator |
| */ |
| public static MatrixSIS createDimensionSelect(final int sourceDimensions, final int[] selectedDimensions) { |
| final int numTargetDim = selectedDimensions.length; |
| final MatrixSIS matrix = createZero(numTargetDim+1, sourceDimensions+1); |
| for (int j=0; j<numTargetDim; j++) { |
| final int i = selectedDimensions[j]; |
| ArgumentChecks.ensureValidIndex(sourceDimensions, i); |
| matrix.setElement(j, i, 1); |
| } |
| matrix.setElement(numTargetDim, sourceDimensions, 1); |
| return matrix; |
| } |
| |
| /** |
| * Creates a matrix which converts a subset of coordinates using the transform given by another matrix. |
| * For example giving (<var>latitude</var>, <var>longitude</var>, <var>height</var>) coordinates, |
| * a pass through operation can convert the height values from feet to metres without affecting |
| * the (<var>latitude</var>, <var>longitude</var>) values. |
| * |
| * <p>The given sub-matrix shall have the following properties:</p> |
| * <ul> |
| * <li>The last row often (but not necessarily) contains 0 values everywhere except in the last column.</li> |
| * <li>Values in the last column are translation terms, except in the last row.</li> |
| * <li>All other values are scale or shear terms.</li> |
| * </ul> |
| * |
| * A square matrix complying with the above conditions is often {@linkplain #isAffine(Matrix) affine}, |
| * but this is not mandatory |
| * (for example a <cite>perspective transform</cite> may contain non-zero values in the last row). |
| * |
| * <p>This method builds a new matrix with the following content:</p> |
| * <ul> |
| * <li>An amount of {@code firstAffectedCoordinate} rows and columns are inserted before the first |
| * row and columns of the sub-matrix. The elements for the new rows and columns are set to 1 |
| * on the diagonal, and 0 elsewhere.</li> |
| * <li>The sub-matrix - except for its last row and column - is copied in the new matrix starting |
| * at index ({@code firstAffectedCoordinate}, {@code firstAffectedCoordinate}).</li> |
| * <li>An amount of {@code numTrailingCoordinates} rows and columns are appended after the above sub-matrix. |
| * Their elements are set to 1 on the pseudo-diagonal ending in the lower-right corner, and 0 elsewhere.</li> |
| * <li>The last sub-matrix row is copied in the last row of the new matrix, and the last sub-matrix column |
| * is copied in the last column of the sub-matrix.</li> |
| * </ul> |
| * |
| * <div class="note"><b>Example:</b> |
| * given the following sub-matrix which converts height values from feet to metres before to subtracts 25 metres: |
| * |
| * {@preformat math |
| * ┌ ┐ ┌ ┐ ┌ ┐ |
| * │ z' │ = │ 0.3048 -25 │ × │ z │ |
| * │ 1 │ │ 0 1 │ │ 1 │ |
| * └ ┘ └ ┘ └ ┘ |
| * } |
| * |
| * Then a call to {@code Matrices.createPassThrough(2, subMatrix, 1)} will return the following matrix, |
| * which can be used for converting the height (<var>z</var>) without affecting the other coordinate values |
| * (<var>x</var>,<var>y</var>,<var>t</var>): |
| * |
| * {@preformat math |
| * ┌ ┐ ┌ ┐ ┌ ┐ |
| * │ x │ │ 1 0 0 0 0 │ │ x │ |
| * │ y │ │ 0 1 0 0 0 │ │ y │ |
| * │ z' │ = │ 0 0 0.3048 0 -25 │ × │ z │ |
| * │ t │ │ 0 0 0 1 0 │ │ t │ |
| * │ 1 │ │ 0 0 0 0 1 │ │ 1 │ |
| * └ ┘ └ ┘ └ ┘ |
| * } |
| * </div> |
| * |
| * @param firstAffectedCoordinate the lowest index of the affected coordinates. |
| * @param subMatrix the matrix to use for affected coordinates. |
| * @param numTrailingCoordinates number of trailing coordinates to pass through. |
| * @return a matrix for the same transform than the given matrix, |
| * augmented with leading and trailing pass-through coordinates. |
| * |
| * @see org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory#createPassThroughTransform(int, MathTransform, int) |
| */ |
| public static MatrixSIS createPassThrough(final int firstAffectedCoordinate, |
| final Matrix subMatrix, final int numTrailingCoordinates) |
| { |
| ArgumentChecks.ensureNonNull ("subMatrix", subMatrix); |
| ArgumentChecks.ensurePositive("firstAffectedCoordinate", firstAffectedCoordinate); |
| ArgumentChecks.ensurePositive("numTrailingCoordinates", numTrailingCoordinates); |
| final int expansion = firstAffectedCoordinate + numTrailingCoordinates; |
| int sourceDimensions = subMatrix.getNumCol(); // Will become the number of dimensions later. |
| int targetDimensions = subMatrix.getNumRow(); |
| /* |
| * Get data from the source matrix, together with the error terms if present. |
| * The 'stride' and 'length' values will be used for computing indices in that array. |
| * The DoubleDouble temporary object is used only if the array contains error terms. |
| */ |
| final int stride = sourceDimensions; |
| final int length = sourceDimensions * targetDimensions; |
| final double[] sources = getExtendedElements(subMatrix); |
| final DoubleDouble transfer = (sources.length > length) ? new DoubleDouble() : null; |
| final MatrixSIS matrix = createZero(targetDimensions-- + expansion, |
| sourceDimensions-- + expansion, |
| transfer != null); |
| /* |
| * Following code processes from upper row to lower row. |
| * First, set the diagonal elements on leading new dimensions. |
| */ |
| for (int j=0; j<firstAffectedCoordinate; j++) { |
| matrix.setElement(j, j, 1); |
| } |
| /* |
| * Copy the sub-matrix, with special case for the translation terms |
| * which are unconditionally stored in the last column. |
| */ |
| final int lastColumn = sourceDimensions + expansion; |
| matrix.setElements(sources, length, stride, transfer, |
| 0, 0, // Source (row, colum) |
| firstAffectedCoordinate, firstAffectedCoordinate, // Target (row, column) |
| targetDimensions, sourceDimensions); // Number of rows and columns to copy. |
| matrix.setElements(sources, length, stride, transfer, |
| 0, sourceDimensions, // Source (row, colum): last column |
| firstAffectedCoordinate, lastColumn, // Target (row, column): part of last column |
| targetDimensions, 1); // Copy some rows of only 1 column. |
| /* |
| * Set the pseudo-diagonal elements on the trailing new dimensions. |
| * 'diff' is zero for a square matrix and non-zero for rectangular matrix. |
| */ |
| final int diff = targetDimensions - sourceDimensions; |
| for (int i=lastColumn - numTrailingCoordinates; i<lastColumn; i++) { |
| matrix.setElement(diff + i, i, 1); |
| } |
| /* |
| * Copy the last row from the sub-matrix. In the usual affine transform, |
| * this row contains only 0 element except for the last one, which is 1. |
| */ |
| final int lastRow = targetDimensions + expansion; |
| matrix.setElements(sources, length, stride, transfer, |
| targetDimensions, 0, // Source (row, colum): last row |
| lastRow, firstAffectedCoordinate, // Target (row, column): part of last row |
| 1, sourceDimensions); // Copy some columns of only 1 row. |
| matrix.setElements(sources, length, stride, transfer, |
| targetDimensions, sourceDimensions, |
| lastRow, lastColumn, |
| 1, 1); |
| return matrix; |
| } |
| |
| /** |
| * Returns a matrix with the same content than the given matrix but a different size, assuming an affine transform. |
| * This method can be invoked for adding or removing the <strong>last</strong> dimensions of an affine transform. |
| * More specifically: |
| * |
| * <ul class="verbose"> |
| * <li>If the given {@code numCol} is <var>n</var> less than the number of columns in the given matrix, |
| * then the <var>n</var> columns <em>before the last column</em> are removed. |
| * The last column is left unchanged because it is assumed to contain the translation terms.</li> |
| * <li>If the given {@code numCol} is <var>n</var> more than the number of columns in the given matrix, |
| * then <var>n</var> columns are inserted <em>before the last column</em>. |
| * All values in the new columns will be zero.</li> |
| * <li>If the given {@code numRow} is <var>n</var> less than the number of rows in the given matrix, |
| * then the <var>n</var> rows <em>before the last row</em> are removed. |
| * The last row is left unchanged because it is assumed to contain the usual [0 0 0 … 1] terms.</li> |
| * <li>If the given {@code numRow} is <var>n</var> more than the number of rows in the given matrix, |
| * then <var>n</var> rows are inserted <em>before the last row</em>. |
| * The corresponding offset and scale factors will be 0 and 1 respectively. |
| * In other words, new dimensions are propagated unchanged.</li> |
| * </ul> |
| * |
| * @param matrix the matrix to resize. This matrix will never be changed. |
| * @param numRow the new number of rows. This is equal to the desired number of target dimensions plus 1. |
| * @param numCol the new number of columns. This is equal to the desired number of source dimensions plus 1. |
| * @return a new matrix of the given size, or the given {@code matrix} if no resizing was needed. |
| */ |
| public static Matrix resizeAffine(final Matrix matrix, int numRow, int numCol) { |
| ArgumentChecks.ensureNonNull ("matrix", matrix); |
| ArgumentChecks.ensureStrictlyPositive("numRow", numRow); |
| ArgumentChecks.ensureStrictlyPositive("numCol", numCol); |
| int srcRow = matrix.getNumRow(); |
| int srcCol = matrix.getNumCol(); |
| if (numRow == srcRow && numCol == srcCol) { |
| return matrix; |
| } |
| final int stride = srcCol; |
| final int length = srcCol * srcRow; |
| final double[] sources = getExtendedElements(matrix); |
| final DoubleDouble transfer = (sources.length > length) ? new DoubleDouble() : null; |
| final MatrixSIS resized = createZero(numRow, numCol, transfer != null); |
| final int copyRow = Math.min(--numRow, --srcRow); |
| final int copyCol = Math.min(--numCol, --srcCol); |
| for (int j=copyRow; j<numRow; j++) { |
| resized.setElement(j, j, 1); |
| } |
| resized.setElements(sources, length, stride, transfer, 0, 0, 0, 0, copyRow, copyCol); // Shear and scale terms. |
| resized.setElements(sources, length, stride, transfer, 0, srcCol, 0, numCol, copyRow, 1); // Translation column. |
| resized.setElements(sources, length, stride, transfer, srcRow, 0, numRow, 0, 1, copyCol); // Last row. |
| resized.setElements(sources, length, stride, transfer, srcRow, srcCol, numRow, numCol, 1, 1); // Last row. |
| return resized; |
| } |
| |
| /** |
| * Returns {@code true} if the given matrix is likely to use extended precision. |
| * A value of {@code true} is not a guarantee that the matrix uses extended precision, |
| * but a value of {@code false} is a guarantee that it does not. |
| */ |
| private static boolean isExtendedPrecision(final Matrix matrix) { |
| return (matrix instanceof MatrixSIS) ? ((MatrixSIS) matrix).isExtendedPrecision() : |
| (matrix instanceof ExtendedPrecisionMatrix); // Not guarantee that the matrix really uses double-double. |
| } |
| |
| /** |
| * Returns the elements of the given matrix, together with error terms if available. |
| */ |
| private static double[] getExtendedElements(final Matrix matrix) { |
| if (matrix instanceof ExtendedPrecisionMatrix) { |
| return ((ExtendedPrecisionMatrix) matrix).getExtendedElements(); |
| } else { |
| return MatrixSIS.castOrCopy(matrix).getElements(); |
| } |
| } |
| |
| /** |
| * Creates a new matrix which is a copy of the given matrix. |
| * |
| * <div class="note"><b>Implementation note:</b> |
| * For square matrix with a size between {@value org.apache.sis.referencing.operation.matrix.Matrix1#SIZE} |
| * and {@value org.apache.sis.referencing.operation.matrix.Matrix4#SIZE} inclusive, the returned matrix is |
| * usually an instance of one of {@link Matrix1} … {@link Matrix4} subtypes.</div> |
| * |
| * @param matrix the matrix to copy, or {@code null}. |
| * @return a copy of the given matrix, or {@code null} if the given matrix was null. |
| * |
| * @see MatrixSIS#clone() |
| * @see MatrixSIS#castOrCopy(Matrix) |
| */ |
| public static MatrixSIS copy(final Matrix matrix) { |
| if (matrix == null) { |
| return null; |
| } |
| final int size = matrix.getNumRow(); |
| if (size != matrix.getNumCol()) { |
| return new NonSquareMatrix(matrix); |
| } |
| if (!isExtendedPrecision(matrix)) { |
| switch (size) { |
| case 1: return new Matrix1(matrix); |
| case 2: return new Matrix2(matrix); |
| case 3: return new Matrix3(matrix); |
| case 4: return new Matrix4(matrix); |
| } |
| } |
| return new GeneralMatrix(matrix); |
| } |
| |
| /** |
| * Returns an unmodifiable view of the given matrix. The returned matrix is immutable |
| * only if the given {@code matrix} is not modified anymore after this method call. |
| * |
| * @param matrix the matrix for which to get an unmodifiable view, or {@code null}. |
| * @return a unmodifiable view of the given matrix, or {@code null} if the given matrix was null. |
| * |
| * @since 0.6 |
| */ |
| public static MatrixSIS unmodifiable(final Matrix matrix) { |
| if (matrix == null || matrix instanceof UnmodifiableMatrix) { |
| return (MatrixSIS) matrix; |
| } else { |
| return new UnmodifiableMatrix(matrix); |
| } |
| } |
| |
| /** |
| * Returns a new matrix which is the result of multiplying the first matrix with the second one. |
| * In other words, returns {@code m1} × {@code m2}. |
| * |
| * @param m1 the first matrix to multiply. |
| * @param m2 the second matrix to multiply. |
| * @return the result of {@code m1} × {@code m2}. |
| * @throws MismatchedMatrixSizeException if the number of columns in {@code m1} is not equals to the |
| * number of rows in {@code m2}. |
| * |
| * @see MatrixSIS#multiply(Matrix) |
| * |
| * @since 0.6 |
| */ |
| public static MatrixSIS multiply(final Matrix m1, final Matrix m2) throws MismatchedMatrixSizeException { |
| if (m1 instanceof MatrixSIS) { |
| return ((MatrixSIS) m1).multiply(m2); // Maybe the subclass overrides that method. |
| } |
| final int nc = m2.getNumCol(); |
| MatrixSIS.ensureNumRowMatch(m1.getNumCol(), m2.getNumRow(), nc); |
| final GeneralMatrix result = GeneralMatrix.createExtendedPrecision(m1.getNumRow(), nc, false); |
| result.setToProduct(m1, m2); |
| return result; |
| } |
| |
| /** |
| * Returns the inverse of the given matrix. |
| * |
| * @param matrix the matrix to inverse, or {@code null}. |
| * @return the inverse of this matrix, or {@code null} if the given matrix was null. |
| * @throws NoninvertibleMatrixException if the given matrix is not invertible. |
| * |
| * @see MatrixSIS#inverse() |
| * |
| * @since 0.6 |
| */ |
| public static MatrixSIS inverse(final Matrix matrix) throws NoninvertibleMatrixException { |
| if (matrix == null) { |
| return null; |
| } else if (matrix instanceof MatrixSIS) { |
| return ((MatrixSIS) matrix).inverse(); // Maybe the subclass override that method. |
| } else if (matrix.getNumRow() != matrix.getNumCol()) { |
| return new NonSquareMatrix(matrix).inverse(); |
| } else { |
| return Solver.inverse(matrix, true); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the given matrix represents an affine transform. |
| * A transform is affine if the matrix is square and its last row contains |
| * only zeros, except in the last column which contains 1. |
| * |
| * @param matrix the matrix to test. |
| * @return {@code true} if the matrix represents an affine transform. |
| * |
| * @see MatrixSIS#isAffine() |
| */ |
| public static boolean isAffine(final Matrix matrix) { |
| if (matrix instanceof MatrixSIS) { |
| return ((MatrixSIS) matrix).isAffine(); |
| } else { |
| return MatrixSIS.isAffine(matrix); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the given matrix represents a translation. |
| * This method returns {@code true} if the given matrix {@linkplain #isAffine(Matrix) is affine} |
| * and differs from the identity matrix only in the last column. |
| * |
| * @param matrix the matrix to test. |
| * @return {@code true} if the matrix represents a translation. |
| * |
| * @since 0.7 |
| */ |
| public static boolean isTranslation(final Matrix matrix) { |
| if (!isAffine(matrix)) { |
| return false; |
| } |
| final int numRow = matrix.getNumRow() - 1; // Excluding translation column. |
| final int numCol = matrix.getNumCol() - 1; // Excluding last row in affine transform. |
| for (int j=0; j<numRow; j++) { |
| for (int i=0; i<numCol; i++) { |
| if (matrix.getElement(j,i) != ((i == j) ? 1 : 0)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns {@code true} if the given matrix is close to an identity matrix, given a tolerance threshold. |
| * This method is equivalent to computing the difference between the given matrix and an identity matrix |
| * of identical size, and returning {@code true} if and only if all differences are smaller than or equal |
| * to {@code tolerance}. |
| * |
| * <p><b>Caution:</b> {@linkplain org.apache.sis.referencing.datum.BursaWolfParameters Bursa-Wolf parameters}, |
| * when represented as a matrix, are close to an identity transform and could easily be confused with rounding |
| * errors. In case of doubt, it is often safer to use the strict {@link MatrixSIS#isIdentity()} method instead |
| * than this one.</p> |
| * |
| * @param matrix the matrix to test for identity. |
| * @param tolerance the tolerance value, or 0 for a strict comparison. |
| * @return {@code true} if this matrix is close to the identity matrix given the tolerance threshold. |
| * |
| * @see MatrixSIS#isIdentity() |
| */ |
| public static boolean isIdentity(final Matrix matrix, final double tolerance) { |
| final int numRow = matrix.getNumRow(); |
| final int numCol = matrix.getNumCol(); |
| if (numRow != numCol) { |
| return false; |
| } |
| for (int j=0; j<numRow; j++) { |
| for (int i=0; i<numCol; i++) { |
| double e = matrix.getElement(j,i); |
| if (i == j) { |
| e--; |
| } |
| if (!(Math.abs(e) <= tolerance)) { // Uses '!' in order to catch NaN values. |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Compares the given matrices for equality, using the given relative or absolute tolerance threshold. |
| * The matrix elements are compared as below: |
| * |
| * <ul> |
| * <li>{@link Double#NaN} values are considered equals to all other NaN values</li> |
| * <li>Infinite values are considered equal to other infinite values of the same sign</li> |
| * <li>All other values are considered equal if the absolute value of their difference is |
| * smaller than or equals to the threshold described below.</li> |
| * </ul> |
| * |
| * If {@code relative} is {@code true}, then for any pair of values <var>v1</var><sub>j,i</sub> |
| * and <var>v2</var><sub>j,i</sub> to compare, the tolerance threshold is scaled by |
| * {@code max(abs(v1), abs(v2))}. Otherwise the threshold is used as-is. |
| * |
| * @param m1 the first matrix to compare, or {@code null}. |
| * @param m2 the second matrix to compare, or {@code null}. |
| * @param epsilon the tolerance value. |
| * @param relative if {@code true}, then the tolerance value is relative to the magnitude |
| * of the matrix elements being compared. |
| * @return {@code true} if the values of the two matrix do not differ by a quantity greater |
| * than the given tolerance threshold. |
| * |
| * @see MatrixSIS#equals(Matrix, double) |
| */ |
| public static boolean equals(final Matrix m1, final Matrix m2, final double epsilon, final boolean relative) { |
| if (m1 != m2) { |
| if (m1 == null || m2 == null) { |
| return false; |
| } |
| final int numRow = m1.getNumRow(); |
| if (numRow != m2.getNumRow()) { |
| return false; |
| } |
| final int numCol = m1.getNumCol(); |
| if (numCol != m2.getNumCol()) { |
| return false; |
| } |
| for (int j=0; j<numRow; j++) { |
| for (int i=0; i<numCol; i++) { |
| final double v1 = m1.getElement(j, i); |
| final double v2 = m2.getElement(j, i); |
| double tolerance = epsilon; |
| if (relative) { |
| tolerance *= Math.max(Math.abs(v1), Math.abs(v2)); |
| } |
| if (!(Math.abs(v1 - v2) <= tolerance)) { |
| if (Numerics.equals(v1, v2)) { |
| // Special case for NaN and infinite values. |
| continue; |
| } |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Compares the given matrices for equality, using the given comparison strictness level. |
| * To be considered equal, the two matrices must met the following conditions, which depend |
| * on the {@code mode} argument: |
| * |
| * <ul> |
| * <li>{@link ComparisonMode#STRICT STRICT}: |
| * the two matrices must be of the same class, have the same size and the same element values.</li> |
| * <li>{@link ComparisonMode#BY_CONTRACT BY_CONTRACT}: |
| * the two matrices must have the same size and the same element values, |
| * but are not required to be the same implementation class (any {@link Matrix} is okay).</li> |
| * <li>{@link ComparisonMode#IGNORE_METADATA IGNORE_METADATA}: same as {@code BY_CONTRACT}, |
| * since matrices have no metadata.</li> |
| * <li>{@link ComparisonMode#APPROXIMATE APPROXIMATE}: |
| * the two matrices must have the same size, but the element values can differ up to some threshold. |
| * The threshold value is determined empirically and may change in any future SIS versions. |
| * For more control, use {@link #equals(Matrix, Matrix, double, boolean)} instead.</li> |
| * </ul> |
| * |
| * @param m1 the first matrix to compare, or {@code null}. |
| * @param m2 the second matrix to compare, or {@code null}. |
| * @param mode the strictness level of the comparison. |
| * @return {@code true} if both matrices are equal. |
| * |
| * @see MatrixSIS#equals(Object, ComparisonMode) |
| */ |
| public static boolean equals(final Matrix m1, final Matrix m2, final ComparisonMode mode) { |
| switch (mode) { |
| case STRICT: return Objects.equals(m1, m2); |
| case BY_CONTRACT: // Fall through |
| case IGNORE_METADATA: return equals(m1, m2, 0, false); |
| case DEBUG: // Fall through |
| case ALLOW_VARIANT: // Fall through |
| case APPROXIMATE: return equals(m1, m2, Numerics.COMPARISON_THRESHOLD, true); |
| default: throw new IllegalArgumentException(Errors.format( |
| Errors.Keys.UnknownEnumValue_2, ComparisonMode.class, mode)); |
| } |
| } |
| |
| /** |
| * Returns a unlocalized string representation of the given matrix. |
| * For each column, the numbers are aligned on the decimal separator. |
| * |
| * <p>The current implementation formats ±0 and ±1 without trailing {@code ".0"}, and all other values with |
| * a per-column uniform number of fraction digits. The ±0 and ±1 values are treated especially because they |
| * usually imply a <cite>"no scale"</cite>, <cite>"no translation"</cite> or <cite>"orthogonal axes"</cite> |
| * meaning. A matrix in SIS is often populated mostly by ±0 and ±1 values, with a few "interesting" values. |
| * The simpler ±0 and ±1 formatting makes easier to spot the "interesting" values.</p> |
| * |
| * <p>The following example shows the string representation of an affine transform which swap |
| * (<var>latitude</var>, <var>longitude</var>) axes, converts degrees to radians and converts |
| * height values from feet to metres:</p> |
| * |
| * {@preformat math |
| * ┌ ┐ |
| * │ 0 0.017453292519943295 0 0 │ |
| * │ 0.017453292519943295 0 0 0 │ |
| * │ 0 0 0.3048 0 │ |
| * │ 0 0 0 1 │ |
| * └ ┘ |
| * } |
| * |
| * <div class="note"><b>Note:</b> |
| * Formatting on a per-column basis is convenient for the kind of matrices used in referencing by coordinates, |
| * because each column is typically a displacement vector in a different dimension of the source coordinate |
| * reference system. In addition, the last column is often a translation vector having a magnitude very |
| * different than the other columns.</div> |
| * |
| * @param matrix the matrix for which to get a string representation. |
| * @return a string representation of the given matrix. |
| */ |
| public static String toString(final Matrix matrix) { |
| final int numRow = matrix.getNumRow(); |
| final int numCol = matrix.getNumCol(); |
| final String[] elements = new String [numCol * numRow]; // String representation of matrix values. |
| final boolean[] noFractionDigits = new boolean[numCol * numRow]; // Whether to remove the trailing ".0" for a given number. |
| final boolean[] hasDecimalSeparator = new boolean[numCol]; // Whether the column has at least one number where fraction digits are shown. |
| final byte[] maximumFractionDigits = new byte [numCol]; // The greatest amount of fraction digits found in a column. |
| final byte[] maximumPaddingZeros = new byte [numCol * numRow]; // Maximal amount of zeros that we can append before to exceed the IEEE 754 accuracy. |
| final byte[] widthBeforeFraction = new byte [numCol]; // Number of characters before the fraction digits: spacing + ('-') + integerDigits + '.' |
| final byte[] columnWidth = new byte [numCol]; // Total column width. |
| int totalWidth = 1; |
| /* |
| * Create now the string representation of all matrix elements and measure the width |
| * of the integer field and the fraction field, then the total width of each column. |
| */ |
| int spacing = 1; // Spacing is 1 before the first column only, then SPACING for other columns. |
| for (int i=0; i<numCol; i++) { |
| for (int j=0; j<numRow; j++) { |
| final int flatIndex = j*numCol + i; |
| final double value = matrix.getElement(j,i); |
| String element = Double.toString(value); |
| final int width; |
| /* |
| * Special case for ±0 and ±1 (because those values appear very often and have |
| * a particular meaning): for those values, we will ignore the fraction digits. |
| * For all other values, we will format all fraction digits. |
| */ |
| if (value == -1 || value == 0 || value == +1) { |
| noFractionDigits[flatIndex] = true; |
| width = spacing + element.length() - 2; // The -2 is for ignoring the trailing ".0" |
| widthBeforeFraction[i] = (byte) Math.max(widthBeforeFraction[i], width); |
| } else { |
| /* |
| * All values other than ±0 and ±1. If the values is NaN or infinity (in which case there is |
| * no decimal separator), give all spaces to the "before fraction" side for right-alignment. |
| */ |
| int s = element.lastIndexOf('.'); |
| if (s < 0) { |
| element = CharSequences.replace(element, "Infinity", "∞").toString(); |
| width = spacing + element.length(); |
| widthBeforeFraction[i] = (byte) Math.max(widthBeforeFraction[i], width); |
| } else { |
| /* |
| * All values other than ±0, ±1, NaN and infinity. We store separately the width before |
| * and after the decimal separator. The width before the separator contains the spacing |
| * between cells. |
| */ |
| hasDecimalSeparator[i] = true; |
| final int numFractionDigits = element.length() - ++s; |
| width = (widthBeforeFraction [i] = (byte) Math.max(widthBeforeFraction [i], spacing + s)) |
| + (maximumFractionDigits[i] = (byte) Math.max(maximumFractionDigits[i], numFractionDigits)); |
| /* |
| * If the number use exponential notation, we will not be allowed to append any zero. |
| * Otherwise we will append some zeros for right-alignment, but without exceeding the |
| * IEEE 754 'double' accuracy for not giving a false sense of precision. |
| */ |
| if (element.indexOf('E') < 0) { |
| final int accuracy = -DecimalFunctions.floorLog10(Math.ulp(value)); |
| maximumPaddingZeros[flatIndex] = (byte) (accuracy - numFractionDigits); |
| } |
| } |
| } |
| columnWidth[i] = (byte) Math.max(columnWidth[i], width); |
| elements[flatIndex] = element; |
| } |
| totalWidth += columnWidth[i]; |
| spacing = SPACING; // Spacing before all columns after the first one. |
| } |
| /* |
| * Now append the formatted elements with the appropriate amount of spaces before each value, |
| * and trailling zeros after each value except ±0, ±1, NaN and infinities. |
| */ |
| final String lineSeparator = System.lineSeparator(); |
| final CharSequence whiteLine = CharSequences.spaces(totalWidth); |
| final StringBuilder buffer = new StringBuilder((totalWidth + 2 + lineSeparator.length()) * (numRow + 2)); |
| buffer.append('┌').append(whiteLine).append('┐').append(lineSeparator); |
| int flatIndex = 0; |
| for (int j=0; j<numRow; j++) { |
| buffer.append('│'); |
| for (int i=0; i<numCol; i++) { |
| final String element = elements[flatIndex]; |
| final int width = element.length(); |
| int spaces, s = element.lastIndexOf('.'); |
| if (s >= 0) { |
| if (hasDecimalSeparator[i]) s++; |
| spaces = widthBeforeFraction[i] - s; // Number of spaces for alignment on the decimal separator |
| } else { |
| spaces = columnWidth[i] - width; // Number of spaces for right alignment (NaN or ∞ cases) |
| } |
| buffer.append(CharSequences.spaces(spaces)).append(element); |
| if (s >= 0) { |
| /* |
| * Append trailing spaces for ±0 and ±1 values, |
| * or trailing zeros for all other real values. |
| */ |
| s += maximumFractionDigits[i] - width; |
| if (noFractionDigits[flatIndex]) { |
| buffer.setLength(buffer.length() - 2); // Erase the trailing ".0" |
| s += 2; |
| } else { |
| int n = Math.min(s, maximumPaddingZeros[flatIndex]); |
| StringBuilders.repeat(buffer, '0', n); |
| s -= n; |
| } |
| buffer.append(CharSequences.spaces(s)); |
| } |
| flatIndex++; |
| } |
| buffer.append(" │").append(lineSeparator); |
| } |
| return buffer.append('└').append(whiteLine).append('┘').append(lineSeparator).toString(); |
| } |
| } |