sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Allow `ResampledGridCoverage` to work (under some conditions) even if the coordinate operation can not be reduced to 2 dimensions.
Date Thu, 28 May 2020 12:10:54 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 03e05e5  Allow `ResampledGridCoverage` to work (under some conditions) even if the
coordinate operation can not be reduced to 2 dimensions.
03e05e5 is described below

commit 03e05e58c5b5b7c93e41d843b3591e2f2900ef5b
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu May 28 14:10:04 2020 +0200

    Allow `ResampledGridCoverage` to work (under some conditions) even if the coordinate operation
can not be reduced to 2 dimensions.
---
 .../sis/coverage/grid/ResampledGridCoverage.java   |  42 ++++++++-
 .../java/org/apache/sis/image/PixelIterator.java   |   4 +
 .../java/org/apache/sis/image/ResampledImage.java  |   4 +
 .../org/apache/sis/internal/feature/Resources.java |  10 ++
 .../sis/internal/feature/Resources.properties      |   2 +
 .../sis/internal/feature/Resources_fr.properties   |   2 +
 .../coverage/grid/ResampledGridCoverageTest.java   |   7 +-
 .../referencing/factory/sql/EPSGDataAccess.java    |   2 +-
 .../operation/transform/TransformSeparator.java    | 103 +++++++++++++++------
 .../transform/TransformSeparatorTest.java          |  12 +++
 .../java/org/apache/sis/measure/AngleFormat.java   |   4 +-
 11 files changed, 158 insertions(+), 34 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
index f63b4c1..0a171fb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
@@ -31,6 +31,7 @@ import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.util.DoubleDouble;
 import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
@@ -376,7 +377,7 @@ final class ResampledGridCoverage extends GridCoverage {
                 GeneralEnvelope bounds = new GeneralEnvelope(resampled.getEnvelope());
                 bounds.intersect(target.getEnvelope());
                 bounds = Envelopes.transform(targetCornerToCRS.inverse(), bounds);
-                targetExtent = new GridExtent(bounds, GridRoundingMode.ENCLOSING, null, targetExtent,
null);
+                targetExtent = new GridExtent(bounds, GridRoundingMode.NEAREST, null, targetExtent,
null);
                 resampled = new GridGeometry(targetExtent, PixelInCell.CELL_CENTER, targetCenterToCRS,
targetCRS);
                 isGeometryExplicit = true;
             }
@@ -456,7 +457,44 @@ final class ResampledGridCoverage extends GridCoverage {
             final TransformSeparator sep = new TransformSeparator(toSourceCenter);
             sep.addSourceDimensions(resampledDimensions);
             sep.addTargetDimensions(sourceDimensions);
-            final MathTransform toSourceSlice = sep.separate();
+            sep.setSourceExpandable(true);
+            MathTransform toSourceSlice = sep.separate();
+            final int[] requiredSources = sep.getSourceDimensions();
+            if (requiredSources.length > BIDIMENSIONAL) {
+                /*
+                 * If we enter in this block, TransformSeparator can not create a MathTransform
with only the 2
+                 * requested source dimensions; it needs more sources. In such case, if coordinates
in missing
+                 * dimensions can be set to constant values (grid low == grid high), create
a transform which
+                 * will add new dimensions with coordinates set to those constant values.
The example below
+                 * passes the two first dimensions as-is and set the third dimensions to
constant value 7:
+                 *
+                 *     ┌   ┐   ┌         ┐┌   ┐
+                 *     │ x │   │ 1  0  0 ││ x │
+                 *     │ y │ = │ 0  1  0 ││ y │
+                 *     │ z │   │ 0  0  7 ││ 1 │
+                 *     │ 1 │   │ 0  0  1 │└   ┘
+                 *     └   ┘   └         ┘
+                 */
+                final MatrixSIS m = Matrices.createZero(requiredSources.length + 1, BIDIMENSIONAL
+ 1);
+                m.setElement(requiredSources.length, BIDIMENSIONAL, 1);
+                for (int j=0; j < requiredSources.length; j++) {
+                    final int r = requiredSources[j];
+                    final int i = Arrays.binarySearch(resampledDimensions, r);
+                    if (i >= 0) {
+                        m.setElement(j, i, 1);
+                    } else {
+                        final long low = sliceExtent.getLow(r);
+                        if (low == sliceExtent.getHigh(r)) {
+                            m.setElement(j, BIDIMENSIONAL, low);
+                        } else {
+                            throw new CannotEvaluateException(Resources.format(
+                                    Resources.Keys.TransformDependsOnDimension_1,
+                                    sliceExtent.getAxisIdentification(r, r)));
+                        }
+                    }
+                }
+                toSourceSlice = MathTransforms.concatenate(MathTransforms.linear(m), toSourceSlice);
+            }
             /*
              * `this.toSource` is a transform from source cell coordinates to target cell
coordinates.
              * We need a transform from source pixel coordinates to target pixel coordinates
(in images).
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
index cbc7d48..f3e53fd 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -36,6 +36,7 @@ import org.opengis.coverage.grid.SequenceType;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.internal.feature.Resources;
 
 import static java.lang.Math.floorDiv;
 import static org.apache.sis.internal.util.Numerics.ceilDiv;
@@ -185,6 +186,9 @@ public abstract class PixelIterator {
      */
     private static Rectangle intersection(int x, int y, int width, int height, Rectangle
subArea, Dimension window) {
         if (window != null) {
+            if (width <= 0 || height <= 0) {
+                throw new IllegalArgumentException(Resources.format(Resources.Keys.EmptyImage));
+            }
             ArgumentChecks.ensureBetween("window.width",  1, width,  window.width);
             ArgumentChecks.ensureBetween("window.height", 1, height, window.height);
             width  -= (window.width  - 1);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
index 6418239..ab915da 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
@@ -34,6 +34,7 @@ import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
@@ -144,6 +145,9 @@ public class ResampledImage extends ComputedImage {
                              final Interpolation interpolation, final Number[] fillValues)
     {
         super(ImageLayout.DEFAULT.createCompatibleSampleModel(source, bounds), source);
+        if (source.getWidth() <= 0 || source.getHeight() <= 0) {
+            throw new IllegalArgumentException(Resources.format(Resources.Keys.EmptyImage));
+        }
         ArgumentChecks.ensureNonNull("interpolation", interpolation);
         ArgumentChecks.ensureStrictlyPositive("width",  width  = bounds.width);
         ArgumentChecks.ensureStrictlyPositive("height", height = bounds.height);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
index 5a790bf..5a0b823 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
@@ -148,6 +148,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short DependencyNotFound_3 = 8;
 
         /**
+         * Image has zero pixel.
+         */
+        public static final short EmptyImage = 73;
+
+        /**
          * Empty tile or image region.
          */
         public static final short EmptyTileOrImageRegion = 67;
@@ -360,6 +365,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short TooManyQualitatives = 48;
 
         /**
+         * Coordinate operation depends on grid dimension {0}.
+         */
+        public static final short TransformDependsOnDimension_1 = 74;
+
+        /**
          * The {0} geometry library is not available in current runtime environment.
          */
         public static final short UnavailableGeometryLibrary_1 = 21;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
index 3cb001c..fa83b8a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
@@ -37,6 +37,7 @@ CategoryRangeOverlap_4            = The two categories \u201c{0}\u201d and
\u201
 CharacteristicsAlreadyExists_2    = Characteristics \u201c{1}\u201d already exists in attribute
\u201c{0}\u201d.
 CharacteristicsNotFound_2         = No characteristics named \u201c{1}\u201d has been found
in \u201c{0}\u201d attribute.
 DependencyNotFound_3              = Operation \u201c{0}\u201d requires a \u201c{1}\u201d
property, but no such property has been found in \u201c{2}\u201d.
+EmptyImage                        = Image has zero pixel.
 EmptyTileOrImageRegion            = Empty tile or image region.
 GridCoordinateOutsideCoverage_4   = Indices ({3}) are outside grid coverage. The value in
dimension {0} shall be between {1,number} and {2,number} inclusive.
 GridEnvelopeMustBeNDimensional_1  = The grid envelope must have at least {0} dimensions.
@@ -77,6 +78,7 @@ PropertyAlreadyExists_2           = Property \u201c{1}\u201d already exists
in f
 PropertyNotFound_2                = No property named \u201c{1}\u201d has been found in \u201c{0}\u201d
feature.
 TileErrorFlagSet_2                = Tile ({0}, {1}) has the error flag set.
 TooManyQualitatives               = Too many qualitative categories.
+TransformDependsOnDimension_1     = Coordinate operation depends on grid dimension {0}.
 UnavailableGeometryLibrary_1      = The {0} geometry library is not available in current
runtime environment.
 UnconvertibleGridCoordinate_2     = Can not convert grid coordinate {1} to type \u2018{0}\u2019.
 UnexpectedNumberOfBands_2         = Expected {0} bands but got {1}.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
index 324af81..0e78e35 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
@@ -42,6 +42,7 @@ CategoryRangeOverlap_4            = Les deux cat\u00e9gories \u00ab\u202f{0}\u20
 CharacteristicsAlreadyExists_2    = La caract\u00e9ristique \u00ab\u202f{1}\u202f\u00bb existe
d\u00e9j\u00e0 dans l\u2019attribut \u00ab\u202f{0}\u202f\u00bb.
 CharacteristicsNotFound_2         = Aucune caract\u00e9ristique nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb
n\u2019a \u00e9t\u00e9 trouv\u00e9e dans l\u2019attribut \u00ab\u202f{0}\u202f\u00bb.
 DependencyNotFound_3              = L\u2019op\u00e9ration \u00ab\u202f{0}\u202f\u00bb n\u00e9cessite
une propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb, mais cette propri\u00e9t\u00e9 n\u2019a
pas \u00e9t\u00e9 trouv\u00e9e dans \u00ab\u202f{2}\u202f\u00bb.
+EmptyImage                        = L\u2019image a z\u00e9ro pixel.
 EmptyTileOrImageRegion            = La tuile ou la r\u00e9gion de l\u2019image est vide.
 GridCoordinateOutsideCoverage_4   = Les indices ({3}) sont en dehors du domaine de la grille.
La valeur dans la dimension {0} doit \u00eatre entre {1,number} et {2,number} inclusivement.
 GridEnvelopeMustBeNDimensional_1  = L\u2019enveloppe de la grille doit avoir au moins {0}
dimensions.
@@ -83,6 +84,7 @@ PropertyAlreadyExists_2           = La propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f
 PropertyNotFound_2                = Aucune propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb
n\u2019a \u00e9t\u00e9 trouv\u00e9e dans l\u2019entit\u00e9 \u00ab\u202f{0}\u202f\u00bb.
 TileErrorFlagSet_2                = La tuile ({0}, {1}) est marqu\u00e9e comme ayant une
erreur.
 TooManyQualitatives               = Trop de cat\u00e9gories qualitatives.
+TransformDependsOnDimension_1     = L\u2019op\u00e9ration sur les coordonn\u00e9es d\u00e9pend
de la dimension {0} de la grille.
 UnavailableGeometryLibrary_1      = La biblioth\u00e8que de g\u00e9om\u00e9tries {0} n\u2019est
pas disponible dans l\u2019environnement d\u2019ex\u00e9cution actuel.
 UnconvertibleGridCoordinate_2     = Ne peut pas convertir la coordonn\u00e9e de grille {1}
vers le type \u2018{0}\u2019.
 UnexpectedNumberOfBands_2         = On attendait {0} bandes mais {1} ont \u00e9t\u00e9 sp\u00e9cifi\u00e9es.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
index 5459f18..ae250e7 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
@@ -464,7 +464,6 @@ public final strictfp class ResampledGridCoverageTest extends TestCase
{
      * @throws TransformException if some coordinates can not be transformed to the target
grid geometry.
      */
     @Test
-    @org.junit.Ignore("Needs more development")
     public void testNonSeparableGridToCRS() throws TransformException {
         final GridCoverage source = createCoverageND(false);
         final MatrixSIS nonSeparableMatrix = Matrices.createDiagonal(4, 4);
@@ -489,14 +488,16 @@ public final strictfp class ResampledGridCoverageTest extends TestCase
{
             }
         }
         final GridGeometry targetGeom = new GridGeometry(
-                source.getGridGeometry().getExtent(),
+                null,           // Let the resample operation compute the extent automatically.
                 CELL_CENTER, nonSeparableG2C,
                 source.getCoordinateReferenceSystem());
         /*
          * Real test is below (above code was only initialization).
+         * The source image is 6×6 but the target image is 7×7 with
+         * the last row and column left black.
          */
         final GridCoverage result = resample(source, targetGeom);
-        assertPixelsEqual(source.render(null), null, result.render(null), null);
+        assertPixelsEqual(source.render(null), null, result.render(null), new Rectangle(2*QS,
2*QS));
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index 472bda7..7dc1e3b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -428,7 +428,7 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements
CRSAutho
         try {
             /*
              * Get the most recent version number from the history table. We get the date
in local timezone
-             * instead then UTC because the date is for information purpose only, and the
local timezone is
+             * instead than UTC because the date is for information purpose only, and the
local timezone is
              * more likely to be shown nicely (without artificial hours) to the user.
              */
             final String query = translator.apply("SELECT VERSION_NUMBER, VERSION_DATE FROM
[Version History]" +
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
index 3e26722..72e38f8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
@@ -49,7 +49,7 @@ import org.apache.sis.util.ArraysExt;
  * The output dimensions can be verified with a call to {@link #getTargetDimensions()}.</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.7
  * @module
  */
@@ -97,6 +97,15 @@ public class TransformSeparator {
     private final MathTransformsOrFactory factory;
 
     /**
+     * Whether {@link #separate()} is allowed to add new dimensions in {@link #sourceDimensions}
+     * if this is required for computing all values specified in {@link #targetDimensions}.
+     *
+     * @see #isSourceExpandable()
+     * @see #setSourceExpandable(boolean)
+     */
+    private boolean isSourceExpandable;
+
+    /**
      * Constructs a separator for the given transform.
      *
      * @param transform  the transform to separate.
@@ -119,13 +128,15 @@ public class TransformSeparator {
 
     /**
      * Resets this transform separator in the same state than after construction. This method
clears any
-     * {@linkplain #getSourceDimensions() source dimensions} and {@linkplain #getTargetDimensions()
target dimensions} settings.
+     * {@linkplain #getSourceDimensions() source} and {@linkplain #getTargetDimensions()
target dimensions}
+     * settings and disables {@linkplain #isSourceExpandable() source expansion}.
      * This method can be invoked when the same {@code MathTransform} needs to be separated
in more than one part,
      * for example an horizontal and a vertical component.
      */
     public void clear() {
-        sourceDimensions = null;
-        targetDimensions = null;
+        sourceDimensions   = null;
+        targetDimensions   = null;
+        isSourceExpandable = false;
     }
 
     /**
@@ -351,6 +362,37 @@ public class TransformSeparator {
     }
 
     /**
+     * Returns whether {@code separate()} is allowed to expand the list of source dimensions.
+     * The default value is {@code false}, which means that {@link #separate()} either returns
+     * a {@link MathTransform} having exactly the requested {@linkplain #getSourceDimensions()
+     * source dimensions}, or throws a {@link FactoryException}.
+     *
+     * @return whether {@code separate()} is allowed to add new source dimensions
+     *         instead of throwing a {@link FactoryException}.
+     *
+     * @since 1.1
+     */
+    public boolean isSourceExpandable() {
+        return isSourceExpandable;
+    }
+
+    /**
+     * Sets whether {@code separate()} is allowed to expand the list of source dimensions.
+     * The default value is {@code false}, which means that {@code separate()} will throw
a {@link FactoryException}
+     * if some {@linkplain #getTargetDimensions() target dimensions} can not be computed
without inputs that are not
+     * in the list of {@linkplain #getSourceDimensions() source dimensions}. If this flag
is set to {@code true},
+     * then {@link #separate()} will be allowed to augment the list of source dimensions
with any inputs that are
+     * essential for producing all requested outputs.
+     *
+     * @param  enabled  whether to allow source dimensions expansion.
+     *
+     * @since 1.1
+     */
+    public void setSourceExpandable(final boolean enabled) {
+        isSourceExpandable = enabled;
+    }
+
+    /**
      * Separates the math transform specified at construction time for given dimension indices.
      * This method creates a math transform that use only the {@linkplain #addSourceDimensions(int...)
specified
      * source dimensions} and return only the {@linkplain #addTargetDimensions(int...) specified
target dimensions}.
@@ -377,7 +419,10 @@ public class TransformSeparator {
      */
     public MathTransform separate() throws FactoryException {
         MathTransform tr = transform;
-        final boolean isSourceSpecified = (sourceDimensions != null);
+        final int[] specifiedSources = sourceDimensions;
+        if (isSourceExpandable) {
+            sourceDimensions = null;                        // Take all sources for now,
will filter later.
+        }
         if (sourceDimensions == null || containsAll(sourceDimensions, 0, tr.getSourceDimensions()))
{
             if (targetDimensions != null && !containsAll(targetDimensions, 0, tr.getTargetDimensions()))
{
                 tr = filterTargetDimensions(tr, targetDimensions);
@@ -395,7 +440,7 @@ public class TransformSeparator {
              */
             final int[] requested = targetDimensions;
             tr = filterSourceDimensions(tr, sourceDimensions);            // May update targetDimensions.
-            assert ArraysExt.isSorted(targetDimensions, true) : "targetDimensions";
+            assert ArraysExt.isSorted(targetDimensions, true);
             if (requested != null) {
                 final int[] inferred = targetDimensions;
                 targetDimensions = requested;
@@ -428,8 +473,8 @@ public class TransformSeparator {
             expected = targetDimensions.length;
             actual   = tr.getTargetDimensions();
             if (actual == expected) {
-                if (!isSourceSpecified) {
-                    tr = removeUnusedSourceDimensions(tr);
+                if (specifiedSources == null || isSourceExpandable) {
+                    tr = removeUnusedSourceDimensions(tr, specifiedSources);
                 }
                 return tr;
             }
@@ -683,44 +728,50 @@ reduce:     for (int j=0; j <= numTgt; j++) {
 
     /**
      * Removes the sources dimensions that are not required for computing the target dimensions.
-     * This method is invoked only if {@link #sourceDimensions} is non-null at {@link #separate()}
invocation time.
+     * This method is invoked only if {@link #sourceDimensions} is null at {@link #separate()}
invocation time.
      * This method can operate only on the first transform of a transformation chain.
      * If this method succeed, then {@link #sourceDimensions} will be updated.
      *
      * <p>This method can process only linear transforms (potentially indirectly through
a concatenated transform).
      * Actually it would be possible to also process pass-through transform followed by a
linear transform, but this
      * case should have been optimized during transform concatenation. If it is not the case,
consider improving the
-     * {@link PassThroughTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}
method instead then
+     * {@link PassThroughTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}
method instead than
      * this one.</p>
      *
-     * @param  head  the first transform of a transformation chain.
+     * @param  head      the first transform of a transformation chain.
+     * @param  required  sources to keep even if not necessary, or {@code null} if none.
      * @return the reduced transform, or {@code head} if this method did not reduced the
transform.
      */
-    private MathTransform removeUnusedSourceDimensions(final MathTransform head) {
+    private MathTransform removeUnusedSourceDimensions(final MathTransform head, final int[]
required) {
         Matrix m = MathTransforms.getMatrix(head);
         if (m != null) {
-            int[] retainedDimensions = ArraysExt.EMPTY_INT;
-            final int dimension = m.getNumCol() - 1;            // Number of source dimensions
(ignore translations column).
-            final int numRows   = m.getNumRow();                // Number of target dimensions
+ 1.
+            final int numRows   = m.getNumRow();            // Number of target dimensions
+ 1.
+            final int dimension = m.getNumCol() - 1;        // Number of source dimensions
(ignore translations column).
+            int   retainedCount = 0;                        // Number of source dimensions
to keep.
+            int[] retainedDimensions = new int[dimension];
             for (int i=0; i<dimension; i++) {
-                for (int j=0; j<numRows; j++) {
-                    if (m.getElement(j,i) != 0) {
-                        // Found a source dimension which is required by target dimension.
-                        final int length = retainedDimensions.length;
-                        retainedDimensions = Arrays.copyOf(retainedDimensions, length+1);
-                        retainedDimensions[length] = i;
-                        break;
+                if (required != null && Arrays.binarySearch(required, i) >= 0)
{
+                    // Dimension to retain unconditionally.
+                    retainedDimensions[retainedCount++] = i;
+                } else {
+                    for (int j=0; j<numRows; j++) {
+                        if (m.getElement(j,i) != 0) {
+                            // Found a source dimension which is required by target dimension.
+                            retainedDimensions[retainedCount++] = i;
+                            break;
+                        }
                     }
                 }
             }
-            if (retainedDimensions.length != dimension) {
+            if (retainedCount != dimension) {
+                retainedDimensions = Arrays.copyOf(retainedDimensions, retainedCount);
                 /*
                  * If we do not retain all dimensions, remove the matrix columns corresponding
to the excluded
                  * source dimensions and create a new transform. We remove consecutive columns
in single calls
                  * to 'removeColumns', from 'lower' inclusive to 'upper' exclusive.
                  */
                 int upper = dimension;
-                for (int i = retainedDimensions.length; --i >= -1;) {
+                for (int i = retainedCount; --i >= -1;) {
                     final int keep = (i >= 0) ? retainedDimensions[i] : -1;
                     final int lower = keep + 1;                                     // First
column to exclude.
                     if (lower != upper) {
@@ -733,7 +784,7 @@ reduce:     for (int j=0; j <= numTgt; j++) {
                  * If the user specified source dimensions, the indices need to be adjusted.
                  * This loop has no effect if all source dimensions were kept before this
method call.
                  */
-                for (int i=0; i<retainedDimensions.length; i++) {
+                for (int i=0; i<retainedCount; i++) {
                     retainedDimensions[i] = sourceDimensions[retainedDimensions[i]];
                 }
                 sourceDimensions = retainedDimensions;
@@ -741,7 +792,7 @@ reduce:     for (int j=0; j <= numTgt; j++) {
             }
         } else if (head instanceof ConcatenatedTransform) {
             final MathTransform transform1 = ((ConcatenatedTransform) head).transform1;
-            final MathTransform reduced = removeUnusedSourceDimensions(transform1);
+            final MathTransform reduced = removeUnusedSourceDimensions(transform1, required);
             if (reduced != transform1) {
                 return MathTransforms.concatenate(reduced, ((ConcatenatedTransform) head).transform2);
             }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
index 806ecfb..fbc6664 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
@@ -186,6 +186,18 @@ public final strictfp class TransformSeparatorTest extends TestCase {
             // This is the expected exception.
             assertNotNull(e.getMessage());
         }
+        /*
+         * Try again, but allow TransformSeparator to expand the list of source dimensions.
+         */
+        s.setSourceExpandable(true);
+        matrix = Matrices.create(3, 4, new double[] {
+            2, 0, 0, 7,
+            0, 5, 0, 6,
+            0, 0, 0, 1
+        });
+        assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(),
STRICT);
+        assertArrayEquals("sourceDimensions", new int[] {0, 1, 2}, s.getSourceDimensions());
+        assertArrayEquals("targetDimensions", new int[] {0, 1},    s.getTargetDimensions());
     }
 
     /**
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
index c09d269..2e49157 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
@@ -1531,7 +1531,7 @@ BigBoss:    switch (skipSuffix(source, pos, DEGREES_FIELD)) {
                 /* ------------------------------------------
                  * STRING ANALYSIS FOLLOWING PRESUMED DEGREES
                  * ------------------------------------------
-                 * Found the seconds suffix instead then the degrees suffix. Move 'degrees'
+                 * Found the seconds suffix instead than the degrees suffix. Move 'degrees'
                  * value to 'seconds' and stop parsing, since seconds are the last field.
                  */
                 case SECONDS_FIELD: {
@@ -1585,7 +1585,7 @@ BigBoss:    switch (skipSuffix(source, pos, DEGREES_FIELD)) {
                         /* ------------------------------------------
                          * STRING ANALYSIS FOLLOWING PRESUMED MINUTES
                          * ------------------------------------------
-                         * Found the seconds suffix instead then the minutes suffix. Move
'minutes'
+                         * Found the seconds suffix instead than the minutes suffix. Move
'minutes'
                          * value to 'seconds' and stop parsing, since seconds are the last
field.
                          */
                         case SECONDS_FIELD: {


Mime
View raw message