/*
 * 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 grid envelopes
     * (which have no CRS) than geodetic envelopes.
     *
     * <div class="section">Spanning the anti-meridian of a Geographic CRS</div>
     * 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.
     *
     * <div class="section">Example</div>
     * 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>
     *
     * <div class="section">Example</div>
     * 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 grid envelopes
     * (which have no CRS) than geodetic envelopes.
     *
     * <div class="section">Spanning the anti-meridian of a Geographic CRS</div>
     * 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();
    }
}
