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 7e1361ad2a Initial fix of the reduction in the number of dimensions of 
`WraparoundTransform`. This is needed for allowing `TransformSeparator` to work 
on it.
7e1361ad2a is described below

commit 7e1361ad2a885a92135764a1d5f344c208c3c06d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jan 23 19:58:04 2026 +0100

    Initial fix of the reduction in the number of dimensions of 
`WraparoundTransform`.
    This is needed for allowing `TransformSeparator` to work on it.
---
 .../operation/transform/TransformJoiner.java       | 180 ++++++++++++++++++++-
 .../operation/transform/WraparoundTransform.java   |  24 ++-
 .../transform/WraparoundTransformTest.java         |  20 +++
 3 files changed, 212 insertions(+), 12 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformJoiner.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformJoiner.java
index 5652422f6d..afa819d7c5 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformJoiner.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformJoiner.java
@@ -16,11 +16,14 @@
  */
 package org.apache.sis.referencing.operation.transform;
 
+import java.util.BitSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Iterator;
 import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.IntFunction;
 import java.util.function.UnaryOperator;
 import org.opengis.util.FactoryException;
@@ -36,6 +39,7 @@ import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.collection.BackingStoreException;
 
 
@@ -66,7 +70,7 @@ import org.apache.sis.util.collection.BackingStoreException;
  * a neighbor transform (at relative index -1 or +1) by a call to {@link 
#replace(int, MathTransform)}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  *
  * @see AbstractMathTransform#tryConcatenate(TransformJoiner)
  *
@@ -484,9 +488,9 @@ valid:  if (i >= 0 && i < steps.size()) {
      * @throws FactoryException if the concatenation is not a valid 
replacement.
      * @throws IllegalArgumentException if a value is repeated twice in the 
given map.
      */
-    public boolean replacePassThrough(Map<Integer,Integer> dimensions) throws 
FactoryException {
+    public boolean replacePassThrough(Map<Integer, Integer> dimensions) throws 
FactoryException {
         Matrix before, after;
-        if (!isAffine(before = getMatrix(-1)) || !isAffine(after = 
getMatrix(+1))) {
+        if (!(isAffine(before = getMatrix(-1)) && isAffine(after = 
getMatrix(+1)))) {
             return false;
         }
         /*
@@ -496,7 +500,7 @@ valid:  if (i >= 0 && i < steps.size()) {
          * the 1 value can be seen as passing-through.
          */
         final var sourceToTarget = new LinkedHashMap<>(dimensions);
-        final var targetToSource = new LinkedHashMap<Integer,Integer>();
+        final var targetToSource = new LinkedHashMap<Integer, Integer>();
         final MathTransform tr   = steps.get(replaceIndex);
         sourceToTarget.put(tr.getSourceDimensions(),
                            tr.getTargetDimensions());   // For the last row 
and the translation column in matrices.
@@ -571,7 +575,7 @@ valid:  if (i >= 0 && i < steps.size()) {
         final MatrixSIS simplified = Matrices.copy(moveFromBeforeToAfter ? 
before : after);
         final int dim = moveFromBeforeToAfter ? after.getNumCol() : 
before.getNumRow();
         final MatrixSIS moved = Matrices.create(dim, dim, 
ExtendedPrecisionMatrix.CREATE_IDENTITY);
-        for (final Map.Entry<Integer,Integer> entry : dimensions.entrySet()) {
+        for (final Map.Entry<Integer, Integer> entry : dimensions.entrySet()) {
             final int srcRow = entry.getKey();
             final int tgtRow = entry.getValue();
             moved.setElement(tgtRow, tgtRow, 0);    // Remove the 1 on the 
diagonal for that row.
@@ -618,7 +622,7 @@ valid:  if (i >= 0 && i < steps.size()) {
      * @param  matrix      the matrix that the caller wants to move if 
possible and useful.
      * @return how far the modified matrix would be from an identity 
transform. Lower is better.
      */
-    private static int removeInvalidPassThrough(final Map<Integer,Integer> 
dimensions, final Matrix matrix,
+    private static int removeInvalidPassThrough(final Map<Integer, Integer> 
dimensions, final Matrix matrix,
                                                 final boolean 
moveFromBeforeToAfter)
     {
         /*
@@ -720,10 +724,172 @@ valid:  if (i >= 0 && i < steps.size()) {
         return (typeOfOriginalMatrix << Short.SIZE) | typeOfSimplifiedMatrix;
     }
 
+    /**
+     * Tries to reduce the number of dimensions of the transform at the 
relative index 0.
+     * This method checks if a neighbor transform changes the number of 
dimensions.
+     * There are two cases:
+     *
+     * <ul>
+     *   <li>Linear transform at relative index +1 reduces the number of 
dimensions.</li>
+     *   <li>Linear transform at relative index -1 increases the number of 
dimensions.</li>
+     * </ul>
+     *
+     * If one of above cases is detected, then this method moves partially the 
linear step on the
+     * other side for reducing the number of dimensions on which the transform 
at index 0 operates.
+     * Such reduction can help {@link TransformSeparator} to separate the 
transform.
+     *
+     * @param  reducer  a constructor accepting selected dimension and 
returning the new transform.
+     * @return whether  the transform chain has been modified as a result of 
this method call.
+     * @throws FactoryException if the concatenation is not a valid 
replacement.
+     */
+    final boolean reduceDimension(final Set<Integer> inputDimensions,
+                                  final Set<Integer> outputDimensions,
+                                  final BiFunction<BitSet, Integer, 
MathTransform> reducer)
+            throws FactoryException
+    {
+        int relativeIndex = +1;
+check:  do {
+            final Matrix matrix = getMatrix(relativeIndex);
+            if (matrix == null) {
+                // Neighbor transform is not linear.
+                continue;
+            }
+            final int numCol = matrix.getNumCol();
+            final int numRow = matrix.getNumRow();
+            if (Integer.signum(numCol - numRow) != relativeIndex) {
+                // Neighbor transform does not change the number of dimensions 
in the desired way.
+                continue;
+            }
+            final MatrixSIS keep = Matrices.createIdentity(Math.min(numCol, 
numRow));
+            final MatrixSIS move = Matrices.copy(matrix);
+            final var rowsToKeep = new BitSet();
+            final var colsToKeep = new BitSet();
+            colsToKeep.set(0, numCol);
+prepare:    if (relativeIndex < 0) {
+                /*
+                 * Case where `matrix` is before the main transform and 
increases the number of dimensions.
+                 * We will try to move `matrix` after the main transform for 
making that transform smaller.
+                 * We only need to keep the rows which modify the input 
coordinates of the main transform.
+                 */
+                inputDimensions.forEach(rowsToKeep::set);
+            } else {
+                /*
+                 * Case where `matrix` is after the main transform and reduces 
the number of dimensions.
+                 * We will try to move `matrix` before the main transform for 
making that transform smaller.
+                 * But we cannot move the rows which depend on the output 
coordinates of the main transform.
+                 */
+                for (int i : outputDimensions) {
+                    for (int j=0; j<numRow; j++) {
+                        if (move.getElement(j, i) != 0) {
+                            rowsToKeep.set(j);
+                        }
+                    }
+                }
+                /*
+                 * Because `move` has too many columns compared to `keep`, we 
will need to ignore some columns.
+                 * We discard the columns where all coefficients are zero, 
starting by the last column before
+                 * translation, until we get the right number of columns.
+                 */
+                if (!discardRowsOrColumns(move, numCol, numRow, colsToKeep, 
true)) {
+                    continue;   // Finished iteration over all columns without 
discarding enough of them.
+                }
+            }
+            /*
+             * Copy into `keep` the rows that we need to keep because they 
compute the input coordinates
+             * needed by the main transform, or because they use the output 
coordinates of main transform.
+             */
+            for (int j=0; (j = rowsToKeep.nextSetBit(j)) >= 0; j++) {
+                for (int k=0, i=0; (i = colsToKeep.nextSetBit(i)) >= 0; i++, 
k++) {
+                    keep.setNumber (j, k, move.getNumber(j, i));
+                    move.setElement(j, i, (j == i) ? 1 : 0);
+                }
+            }
+            /*
+             * Verify that `move` just forwards the `keep` coordinates without 
modification, which is a
+             * necessary condition for executing `move` and `keep` in 
different order. Verify also that
+             * the coordinates modified by `move` will not impact or will not 
be impacted (depending on
+             * whether `move` will be moved to the right or to the left) by 
the main transform.
+             */
+            (relativeIndex < 0 ? outputDimensions : 
inputDimensions).forEach(rowsToKeep::set);
+            // Check input coordinates (columns) because `move` is still a 
step after `keep`.
+            for (int i=0; (i = rowsToKeep.nextSetBit(i)) >= 0; i++) {
+                for (int j=0; j<numRow; j++) {
+                    if (move.getElement(j, i) != (j == i ? 1 : 0)) {
+                        continue check;
+                    }
+                }
+            }
+            /*
+             * Finished the separation. Transforming by `keep` (a square 
matrix) followed by transforming
+             * by `move` (non-square matrix) should be equivalent to 
transforming by the original matrix:
+             *
+             *     assert move × keep = matrix
+             *
+             * Except that we cannot always compute this assertion because of 
the change of matrix size
+             * caused by `colsToKeep`.
+             */
+            final Matrix prefix, suffix;
+            if (relativeIndex < 0) {
+                assert Matrices.equals(matrix, move.multiply(keep), 
Numerics.COMPARISON_THRESHOLD, true) : matrix;
+                if (!discardRowsOrColumns(move, numRow, numCol, colsToKeep, 
false)) {
+                    continue;   // Finished iteration over all rows without 
discarding enough of them.
+                }
+                prefix = keep;
+                suffix = move;
+            } else {
+                prefix = move;
+                suffix = keep;
+            }
+            colsToKeep.clear(numCol - 1);
+            MathTransform concatenation = reducer.apply(colsToKeep, 
relativeIndex);
+            if (concatenation != null) {
+                concatenation = 
factory.createConcatenatedTransform(factory.createAffineTransform(prefix), 
concatenation);
+                concatenation = 
factory.createConcatenatedTransform(concatenation, 
factory.createAffineTransform(suffix));
+                if (replace(relativeIndex, concatenation)) {
+                    return true;
+                }
+            }
+        } while ((relativeIndex = -relativeIndex) < 0);
+        return false;
+    }
+
+    /**
+     * Finds rows or columns to exclude in the given matrix until the expected 
number is reached.
+     * This method expects a {@code keep} argument with all bits set, and will 
clear the bits of
+     * the dimensions to skip.
+     *
+     * By default, this method expects a matrix with more rows than columns 
and will skip rows.
+     * But if {@code transpose} is {@code true}, then the meaning of "row" and 
"column" are swapped,
+     * i.e. this method become used for skipping columns.
+     *
+     * @param  move       the matrix where to look for coefficients.
+     * @param  numRow     number of rows if {@code transpose} is false, or 
number of columns otherwise.
+     * @param  numCol     number of columns if {@code transpose} is false, or 
number of rows otherwise.
+     * @param  keep       the bit set where to clear the bit for rows (or 
columns) to skip.
+     * @param  transpose  whether to swap the rows and columns.
+     * @return {@code true} on success.
+     */
+    private static boolean discardRowsOrColumns(final Matrix move, final int 
numRow, final int numCol,
+                                                final BitSet keep, final 
boolean transpose)
+    {
+skip:   for (int j = numRow - 1; --j >= 0;) {
+            for (int i=0; i<numCol; i++) {
+                if (move.getElement(transpose ? i : j, transpose ? j : i) != 
0) {
+                    continue skip;
+                }
+            }
+            keep.clear(j);
+            if (keep.cardinality() == numCol) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Creates a transform by concatenating two existing transforms.
      * This is a shortcut for {@code factory.createConcatenatedTransform(tr1, 
tr2)},
-     * except that this method reuse existing concatenated transform when 
possible.
+     * except that this method reuses existing concatenated transforms when 
possible.
      *
      * <p>Note that it is not always useful to invoke this method. For 
example, it will usually not bring any benefit
      * compared to direct use of the {@linkplain #factory} if one of the 
arguments is a newly created transform.</p>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
index 401adc6398..ac1100232f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
@@ -16,8 +16,9 @@
  */
 package org.apache.sis.referencing.operation.transform;
 
-import java.util.Objects;
+import java.util.Set;
 import java.util.HashMap;
+import java.util.Objects;
 import java.util.function.Function;
 import java.io.Serializable;
 import org.opengis.util.FactoryException;
@@ -66,7 +67,7 @@ import org.apache.sis.parameter.Parameters;
  * (it may be the [0 … 360]° range, but not necessarily).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  *
  * @see 
org.apache.sis.geometry.Envelopes#transformWithWraparound(MathTransform, 
Envelope)
  *
@@ -405,7 +406,7 @@ public class WraparoundTransform extends 
AbstractMathTransform implements Serial
     }
 
     /**
-     * Concatenates in an optimized way this math transform with the given 
one, if possible.
+     * Optimizes the concatenation of this transform with its neighbor 
transforms, if possible.
      * If this method detects a chain of operations like below:
      *
      * <blockquote>[wraparound]  ⇄  [affine]  ⇄  [wraparound]</blockquote>
@@ -490,9 +491,22 @@ public class WraparoundTransform extends 
AbstractMathTransform implements Serial
                 dimensions.put(i, i);
             }
         }
-        if (!context.replacePassThrough(dimensions)) {
-            super.tryConcatenate(context);
+        if (context.replacePassThrough(dimensions)) {
+            return;
+        }
+        if (getClass() == WraparoundTransform.class) {      // Because we did 
not defined an overrideable `redimension` method yet.
+            final var workOn = Set.of(wraparoundDimension);
+            if (context.reduceDimension(workOn, workOn,
+                    (selected, ignored) -> new WraparoundTransform(
+                            selected.cardinality(),
+                            Math.toIntExact(selected.stream().filter((i) -> i 
< wraparoundDimension).count()),
+                            period,
+                            sourceMedian)))
+            {
+                return;
+            }
         }
+        super.tryConcatenate(context);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java
index 47be8adccc..8da20c22ef 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.referencing.crs.HardCodedCRS;
+import org.apache.sis.referencing.operation.matrix.Matrices;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import static org.opengis.test.Assertions.assertMatrixEquals;
@@ -191,4 +192,23 @@ public final class WraparoundTransformTest extends 
TestCase {
                 1E-15,  // Tolerance
                 "normalize");
     }
+
+    /**
+     * Tests concatenation that reduces the number of dimensions.
+     * The {@link WraparoundTransform#tryConcatenate(TransformJoiner)}
+     * should replace the four-dimensional {@link WraparoundTransform}
+     * by a two-dimensional one.
+     */
+    @Test
+    public void testDimensionReduction() {
+        MathTransform tr = MathTransforms.translation(0, 0, 0, 0);
+        tr = MathTransforms.concatenate(tr, new WraparoundTransform(4, 0, 360, 
40));
+        tr = MathTransforms.concatenate(tr, 
MathTransforms.linear(Matrices.createDimensionSelect(4, new int[] {0, 1})));
+        final var reduced = assertInstanceOf(WraparoundTransform.class, 
MathTransforms.getLastStep(tr));
+        assertEquals(  2, reduced.getSourceDimensions());
+        assertEquals(  2, reduced.getTargetDimensions());
+        assertEquals(  0, reduced.wraparoundDimension);
+        assertEquals(360, reduced.period);
+        assertEquals( 40, reduced.sourceMedian);
+    }
 }

Reply via email to