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
commit 295965c64a044d5b82c7f74c1295c5e7ff33b854 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Mar 25 12:36:05 2023 +0100 Add test for `PassThroughTransform` with non-consecutive modified coordinates. Allow `DefaultPassThroughOperation` to use non-consecutive coordinates at unmarshalling time. --- .../operation/DefaultPassThroughOperation.java | 141 +++++++++++---------- .../operation/transform/MathTransforms.java | 10 +- .../operation/transform/PassThroughTransform.java | 7 +- .../operation/transform/TransformSeparator.java | 26 ++-- .../transform/PassThroughTransformTest.java | 65 ++++++++-- 5 files changed, 156 insertions(+), 93 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java index 4ac1387348..fd62fcd1b8 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java @@ -29,13 +29,11 @@ import org.opengis.referencing.operation.Conversion; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.PassThroughOperation; import org.opengis.referencing.crs.CoordinateReferenceSystem; -import org.opengis.referencing.crs.CompoundCRS; import org.apache.sis.referencing.GeodeticException; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.referencing.operation.transform.PassThroughTransform; import org.apache.sis.internal.referencing.ReferencingUtilities; import org.apache.sis.internal.metadata.ImplementationHelper; -import org.apache.sis.util.UnsupportedImplementationException; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.resources.Errors; @@ -50,7 +48,7 @@ import static org.apache.sis.util.Utilities.deepEquals; * Specifies that a subset of a coordinate tuple is subject to a specific coordinate operation. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.3 + * @version 1.4 * @since 0.6 */ @XmlType(name = "PassThroughOperationType", propOrder = { @@ -62,7 +60,7 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp /** * Serial number for inter-operability with different versions. */ - private static final long serialVersionUID = 4308173919747248695L; + private static final long serialVersionUID = 3516394762777350439L; /** * The operation to apply on the subset of a coordinate tuple. @@ -75,6 +73,16 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp @SuppressWarnings("serial") // Most SIS implementations are serializable. private CoordinateOperation operation; + /** + * Zero-based indices of the modified source coordinates. + * + * <p><b>Consider this field as final!</b> + * This field is modified only at unmarshalling time by {@link #setIndices(int[])}</p> + * + * @see #getModifiedCoordinates() + */ + private int[] modifiedCoordinates; + /** * Constructs a pass-through operation from a set of properties. * The properties given in argument follow the same rules than for the @@ -114,10 +122,27 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp final CoordinateOperation operation, final int firstAffectedCoordinate, final int numTrailingCoordinates) + { + this(properties, sourceCRS, targetCRS, operation, operation.getMathTransform(), firstAffectedCoordinate, numTrailingCoordinates); + } + + /** + * Work around for RFE #4093999 in Sun's bug database + * ("Relax constraint on placement of this()/super() call in constructors"). + */ + private DefaultPassThroughOperation(final Map<String,?> properties, + final CoordinateReferenceSystem sourceCRS, + final CoordinateReferenceSystem targetCRS, + final CoordinateOperation operation, + final MathTransform subTransform, + final int firstAffectedCoordinate, + final int numTrailingCoordinates) { super(properties, sourceCRS, targetCRS, null, - MathTransforms.passThrough(firstAffectedCoordinate, operation.getMathTransform(), numTrailingCoordinates)); + MathTransforms.passThrough(firstAffectedCoordinate, subTransform, numTrailingCoordinates)); this.operation = operation; + modifiedCoordinates = ArraysExt.range(firstAffectedCoordinate, + firstAffectedCoordinate + subTransform.getSourceDimensions()); } /** @@ -134,6 +159,7 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp protected DefaultPassThroughOperation(final PassThroughOperation operation) { super(operation); this.operation = operation.getOperation(); + modifiedCoordinates = operation.getModifiedCoordinates(); } /** @@ -157,10 +183,10 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp * Returns the GeoAPI interface implemented by this class. * The SIS implementation returns {@code PassThroughOperation.class}. * - * <div class="note"><b>Note for implementers:</b> + * <h4>Note for implementers</h4> * Subclasses usually do not need to override this method since GeoAPI does not define {@code PassThroughOperation} * sub-interface. Overriding possibility is left mostly for implementers who wish to extend GeoAPI with their - * own set of interfaces.</div> + * own set of interfaces. * * @return {@code PassThroughOperation.class} or a user-defined sub-interface. */ @@ -192,33 +218,7 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp */ @Override public int[] getModifiedCoordinates() { - final MathTransform transform = super.getMathTransform(); - if (transform instanceof PassThroughTransform) { - return ((PassThroughTransform) transform).getModifiedCoordinates(); - } else if (operation != null) { - /* - * Should not happen with objects created by public methods since the constructor created the transform itself. - * However, may happen with operations parsed from GML. As a fallback, search in the components of CompoundCRS. - * This is not a universal fallback, but works for the most straightforward cases. - */ - final CoordinateReferenceSystem sourceCRS = super.getSourceCRS(); - if (sourceCRS instanceof CompoundCRS) { - int firstAffectedCoordinate = 0; - final CoordinateReferenceSystem search = operation.getSourceCRS(); - for (final CoordinateReferenceSystem c : ((CompoundCRS) sourceCRS).getComponents()) { - final int dim = ReferencingUtilities.getDimension(c); - if (c == search) { - final int[] indices = new int[dim]; - for (int i=0; i<dim; i++) { - indices[i] = firstAffectedCoordinate + i; - } - return indices; - } - firstAffectedCoordinate += dim; - } - } - } - throw new UnsupportedImplementationException(transform.getClass()); + return modifiedCoordinates.clone(); } /** @@ -236,9 +236,13 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp } if (super.equals(object, mode)) { if (mode == ComparisonMode.STRICT) { - return Objects.equals(operation, ((DefaultPassThroughOperation) object).operation); + final var other = (DefaultPassThroughOperation) object; + return Arrays.equals(modifiedCoordinates, other.modifiedCoordinates) + && Objects.equals(operation, other.operation); } else { - return deepEquals(getOperation(), ((PassThroughOperation) object).getOperation(), mode); + final var other = (PassThroughOperation) object; + return Arrays.equals(getModifiedCoordinates(), other.getModifiedCoordinates()) + && deepEquals(getOperation(), other.getOperation(), mode); } } return false; @@ -251,7 +255,7 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp */ @Override protected long computeHashCode() { - return super.computeHashCode() + 31 * operation.hashCode(); + return super.computeHashCode() + 37 * operation.hashCode() + Arrays.hashCode(modifiedCoordinates); } /** @@ -326,6 +330,8 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp * Invoked by JAXB at marshalling time for getting the modified coordinates. * This method converts the zero-based indices to 1-based indices. * + * @return one-based indices of the modified source coordinates. + * * @see #getModifiedCoordinates() */ @XmlElement(name = "modifiedCoordinate", required = true) @@ -339,21 +345,37 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp /** * Invoked by JAXB at unmarshalling time for setting the modified coordinates. - * This method needs to be invoked last, even if the {@code <gml:modifiedCoordinate>} - * elements are not last in the GML document. It is the case when using JAXB because - * multiple occurrences of {@code <gml:modifiedCoordinate>} are aggregated in an array. + * + * @param indices one-based indices of the modified source coordinates. + */ + private void setIndices(int[] dimensions) { + if (modifiedCoordinates == null) { + if (dimensions.length != 0) { + modifiedCoordinates = dimensions = dimensions.clone(); + for (int i=0; i<dimensions.length; i++) { + dimensions[i]--; + } + } + } else { + ImplementationHelper.propertyAlreadySet(DefaultPassThroughOperation.class, "setIndices", "modifiedCoordinates"); + } + } + + /** + * Invoked by JAXB after unmarshalling. If needed, this method tries to infer source/target CRS + * of the nested operation from the source/target CRS of the enclosing pass-through operation. */ - private void setIndices(final int[] dimensions) { + @Override + final void afterUnmarshal(Unmarshaller unmarshaller, Object parent) { + super.afterUnmarshal(unmarshaller, parent); /* - * Argument and state validation. + * State validation. The `missing` string will be used in exception message + * at the end of this method if a required component is reported missing. */ - String missing = "modifiedCoordinate"; + final int[] modifiedCoordinates = this.modifiedCoordinates; FactoryException cause = null; - final int n = dimensions.length; - if (n != 0) { - if (!ArraysExt.isRange(dimensions[0], dimensions)) { - throw new GeodeticException(Errors.format(Errors.Keys.CanNotAssign_2, missing, Arrays.toString(dimensions))); - } + String missing = "modifiedCoordinate"; + if (modifiedCoordinates.length != 0) { missing = "sourceCRS"; final CoordinateReferenceSystem sourceCRS = super.getSourceCRS(); if (sourceCRS != null) { @@ -364,17 +386,14 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp if (operation != null) { /* * If the operation is a defining operation, we need to replace it by a full operation. - * After that, we can store the modified coordinate indices in the transform field. */ MathTransform subTransform = operation.getMathTransform(); if (operation instanceof Conversion) { CoordinateReferenceSystem sourceSub = operation.getSourceCRS(); CoordinateReferenceSystem targetSub = operation.getTargetCRS(); if (subTransform == null || sourceSub == null || targetSub == null) try { - final int[] zeroBased = dimensions.clone(); - for (int i=0; i<n; i++) zeroBased[i]--; - if (sourceSub == null) sourceSub = CRS.selectDimensions(sourceCRS, zeroBased); - if (targetSub == null) targetSub = CRS.selectDimensions(targetCRS, zeroBased); + if (sourceSub == null) sourceSub = CRS.selectDimensions(sourceCRS, modifiedCoordinates); + if (targetSub == null) targetSub = CRS.selectDimensions(targetCRS, modifiedCoordinates); operation = DefaultConversion.castOrCopy((Conversion) operation) .specialize(Conversion.class, sourceSub, targetSub, null); subTransform = operation.getMathTransform(); @@ -383,8 +402,8 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp } } if (subTransform != null) { - transform = MathTransforms.passThrough(dimensions[0] - 1, subTransform, - ReferencingUtilities.getDimension(sourceCRS) - dimensions[n-1]); + transform = MathTransforms.passThrough(modifiedCoordinates, subTransform, + ReferencingUtilities.getDimension(sourceCRS)); return; } } @@ -393,16 +412,4 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp } throw new GeodeticException(Errors.format(Errors.Keys.MissingComponentInElement_2, "PassThroughOperation", missing), cause); } - - /** - * Invoked by JAXB after unmarshalling. If needed, this method tries to infer source/target CRS - * of the nested operation from the source/target CRS if the enclosing pass-through operation. - */ - @Override - void afterUnmarshal(Unmarshaller unmarshaller, Object parent) { - super.afterUnmarshal(unmarshaller, parent); - if (transform == null) { - setIndices(ArraysExt.EMPTY_INT); // Cause an exception to be thrown. - } - } } diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java index 27a1f360d5..74c4b9c99c 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java @@ -364,15 +364,16 @@ public final class MathTransforms extends Static { /** * Creates a transform which passes through a subset of coordinates to another transform. - * The list of modified coordinates is specified by the {@code modifiedCoordinates} argument, - * which must be in strictly increasing order. + * The list of modified coordinates is specified by the {@code modifiedCoordinates} argument. + * The array length must be equal to the number of source dimensions of {@code subTransform} + * and all array elements must be in strictly increasing order. * * @param modifiedCoordinates positions in a source coordinate tuple of the coordinates affected by the transform. * @param subTransform the sub-transform to apply on modified coordinates. * @param resultDim total number of source dimensions of the pass-through transform to return. * @return a pass-through transform for the given set of modified coordinates. - * @throws MismatchedDimensionException if the bit set cardinality is not equal - * to the number of source dimensions in {@code subTransform}. + * @throws MismatchedDimensionException if the {@code modifiedCoordinates} array length + * is not equal to the number of source dimensions in {@code subTransform}. * @throws IllegalArgumentException if the index of a modified coordinates is invalid. * * @see PassThroughTransform#create(BitSet, MathTransform, int, MathTransformFactory) @@ -380,6 +381,7 @@ public final class MathTransforms extends Static { * @since 1.4 */ public static MathTransform passThrough(final int[] modifiedCoordinates, final MathTransform subTransform, final int resultDim) { + ArgumentChecks.ensureNonNull("modifiedCoordinates", modifiedCoordinates); final BitSet bitset = new BitSet(); int previous = -1; for (int i=0; i < modifiedCoordinates.length; i++) { diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java index d6d8bc5d10..1ff924203f 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java @@ -222,6 +222,9 @@ public class PassThroughTransform extends AbstractMathTransform implements Seria * int[] modifiedCoordinates = bitset.stream().toArray(); * } * + * The bitset {@linkplain BitSet#cardinality() cardinality}, which is also the length of above array, + * must be equal to the number of source dimensions in the given {@code subTransform}. + * * <h4>Limitation</h4> * If the modified coordinates are not at consecutive positions in source coordinate tuples, * then the current implementation of this method adds the following restrictions: @@ -238,8 +241,8 @@ public class PassThroughTransform extends AbstractMathTransform implements Seria * @param resultDim total number of source dimensions of the pass-through transform to return. * @param factory the factory to use for creating transforms, or {@code null} for the default. * @return a pass-through transform for the given set of modified coordinates. - * @throws MismatchedDimensionException if the bit set cardinality is not equal - * to the number of source dimensions in {@code subTransform}. + * @throws MismatchedDimensionException if the {@code modifiedCoordinates} bitset cardinality + * is not equal to the number of source dimensions in {@code subTransform}. * @throws IllegalArgumentException if the index of a modified coordinates is out of bounds. * @throws FactoryException if an error occurred while creating a transform step. * 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 4ebfacd07d..67616b04fc 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 @@ -143,11 +143,11 @@ public class TransformSeparator { * Inserts the specified {@code dimension} in the specified sequence at a position that preserve increasing order. * This method does nothing if the given dimension already exists in the given array. * - * <div class="note"><b>Note:</b> - * we do not provide public API for this method because we rather encourage bulk operations (adding many values + * <h4>API note</h4> + * We do not provide public API for this method because we rather encourage bulk operations (adding many values * at once), and because the public API does not allow to specify values in random order (for avoiding risk of * confusion as some users could expect the separated transform to use the dimensions in the order he specified - * them).</div> + * them). * * @param sequence the {@link #sourceDimensions} or {@link #targetDimensions} sequence to update. * @param dimension the value to add to the given sequence. @@ -545,7 +545,7 @@ public class TransformSeparator { final MathTransform step1 = filterSourceDimensions(ctr.transform1, dimensions); final MathTransform step2 = filterSourceDimensions(ctr.transform2, targetDimensions); return factory.concatenate(step1, step2); - // Keep the 'targetDimensions' computed by the last step. + // Keep the `targetDimensions` computed by the last step. } /* * Special case for the passthrough transform: if at least one input dimension belong to the pass- @@ -597,9 +597,11 @@ public class TransformSeparator { targetDimensions = target; /* * If all source dimensions not in the sub-transform are consecutive numbers, we can use our passthrough - * transform implementation. The "consecutive numbers" requirement (expressed in the 'if' statement below) + * transform implementation. The "consecutive numbers" requirement (expressed in the `if` statement below) * is a consequence of a limitation in our current implementation: our current passthrough transform does - * not accept arbitrary indices for modified coordinates. + * not accept arbitrary indices for modified coordinates. We can not delegate to the static factory method + * `MathTransforms.passThrough(int[] modifiedCoordinates, ...)` because that method itself relies on this + * `TransformSeparator` for separating the transform components at non-consecutive indices. */ if (containsAll(dimensions, lower, subLower) && containsAll(dimensions, subUpper, upper)) { final int offset = subDimensions[0]; @@ -615,8 +617,8 @@ public class TransformSeparator { final Matrix matrix = MathTransforms.getMatrix(step); if (matrix != null) { targetDimensions = null; - int startOfRow = 0; // Index of next row to be stored in the 'elements' array. - boolean isLastRowAccepted = false; // To be set to 'true' if we complete successfully up to last row. + int startOfRow = 0; // Index of next row to be stored in the `elements` array. + boolean isLastRowAccepted = false; // To be set to `true` if we complete successfully up to last row. final int numFilteredColumns = (dimensions.length + 1); double[] elements = new double[(numTgt + 1) * numFilteredColumns]; reduce: for (int j=0; j <= numTgt; j++) { @@ -632,7 +634,7 @@ reduce: for (int j=0; j <= numTgt; j++) { elements[startOfRow + filteredColumn++] = element; } else if (element != 0) { /* - * Output dimension 'j' depends on one of discarded input dimension 'i'. + * Output dimension `j` depends on one of discarded input dimension `i`. * The whole row will be discarded. */ continue reduce; @@ -641,10 +643,10 @@ reduce: for (int j=0; j <= numTgt; j++) { /* * We reach this point only if we determined that for current matrix row, all dependencies are listed * in the array of source dimensions to keep. The matrix coefficients for that row are copied in the - * 'elements' array. + * `elements` array. */ elements[startOfRow + filteredColumn++] = matrix.getElement(j, numSrc); // Copy the translation term. - assert filteredColumn == numFilteredColumns : filteredColumn; // We should have used all values in the 'dimensions' array. + assert filteredColumn == numFilteredColumns : filteredColumn; // We should have used all values in the `dimensions` array. startOfRow += numFilteredColumns; /* * In an affine transform, the last row is usually [0 0 0 … 1]. @@ -783,7 +785,7 @@ reduce: for (int j=0; j <= numTgt; j++) { /* * 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. + * to `removeColumns`, from `lower` inclusive to `upper` exclusive. */ int upper = dimension; for (int i = retainedCount; --i >= -1;) { diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java index f5d46d38cd..00185fec10 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java @@ -18,6 +18,7 @@ package org.apache.sis.referencing.operation.transform; import java.util.List; import java.util.Arrays; +import java.util.BitSet; import java.util.Random; import org.opengis.util.FactoryException; import org.opengis.referencing.operation.Matrix; @@ -43,7 +44,7 @@ import org.opengis.test.ToleranceModifier; * Tests {@link PassThroughTransform}. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.0 + * @version 1.4 * @since 0.5 */ @DependsOn({ @@ -83,7 +84,7 @@ public final class PassThroughTransformTest extends MathTransformTestCase { * Tests the pass through transform using an identity transform. * The "pass-through" of such transform shall be itself the identity transform. * - * @throws TransformException should never happen. + * @throws TransformException if a test coordinate tuple cannot be transformed. */ @Test public void testIdentity() throws TransformException { @@ -95,7 +96,7 @@ public final class PassThroughTransformTest extends MathTransformTestCase { * Tests the pass-through transform using an affine transform. * The "pass-through" of such transforms are optimized using matrix arithmetic. * - * @throws TransformException should never happen. + * @throws TransformException if a test coordinate tuple cannot be transformed. */ @Test public void testLinear() throws TransformException { @@ -110,7 +111,7 @@ public final class PassThroughTransformTest extends MathTransformTestCase { * Tests the general pass-through transform. * This test uses a non-linear sub-transform for preventing the factory method to optimize. * - * @throws TransformException should never happen. + * @throws TransformException if a test coordinate tuple cannot be transformed. */ @Test public void testPassthrough() throws TransformException { @@ -120,7 +121,7 @@ public final class PassThroughTransformTest extends MathTransformTestCase { /** * Tests the general pass-through transform with a sub-transform going from 3D to 2D points. * - * @throws TransformException should never happen. + * @throws TransformException if a test coordinate tuple cannot be transformed. */ @Test public void testDimensionDecrease() throws TransformException { @@ -131,7 +132,7 @@ public final class PassThroughTransformTest extends MathTransformTestCase { /** * Tests the general pass-through transform with a sub-transform going from 2D to 3D points. * - * @throws TransformException should never happen. + * @throws TransformException if a test coordinate tuple cannot be transformed. */ @Test public void testDimensionIncrease() throws TransformException { @@ -141,6 +142,9 @@ public final class PassThroughTransformTest extends MathTransformTestCase { /** * Tests a pass-through transform built using the given sub-transform. + * This method uses (indirectly) the {@link PassThroughTransform#create(int, MathTransform, int)} + * factory method with various {@code firstAffectedCoordinate} and {@code numTrailingCoordinates} + * argument values. * * @param subTransform the sub-transform to use for building pass-through transform. * @param expectedClass the expected implementation class of pass-through transforms. @@ -175,9 +179,10 @@ public final class PassThroughTransformTest extends MathTransformTestCase { /** * Tests the current {@linkplain #transform transform} using an array of random coordinate values, - * and compares the result against the expected ones. This method computes itself the expected results. + * and compares the result against the expected ones. This method computes itself the expected results + * on the assumption that all modified coordinates are consecutive. * - * @param subTransform the sub transform used by the current pass-through transform. + * @param subTransform the sub transform used by the current pass-through transform. * @param firstAffectedCoordinate first input/output dimension used by {@code subTransform}. * @throws TransformException if a transform failed. */ @@ -300,4 +305,48 @@ public final class PassThroughTransformTest extends MathTransformTestCase { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), null); } + + /** + * Tests the creation of a pass-through transform with modified coordinates that are not consecutive. + * This is a test of {@link PassThroughTransform#create(BitSet, MathTransform, int, MathTransformFactory)} + * factory method. + * + * @throws FactoryException if an error occurred while combining the transforms. + * @throws TransformException if a test coordinate tuple cannot be transformed. + */ + @Test + public void testNonConsecutiveModifiedCoordinates() throws FactoryException, TransformException { + random = TestUtilities.createRandomNumberGenerator(); + /* + * First, create a pass-through transform from an inseparable `PseudoTransform`. + * Because `PseudoTransform` is inseparable, the modified coordinates must be consecutive. + * However the `PassThroughTransform` result is partially separable and used in next step. + */ + final var bitset = new BitSet(); + bitset.set(1, 3, true); // Modified coordinates = {1, 2}. + MathTransform subTransform = new PseudoTransform(2, 2); + transform = PassThroughTransform.create(bitset, subTransform, 5, null); + assertEquals(5, transform.getSourceDimensions()); + assertEquals(5, transform.getTargetDimensions()); + assertEquals(1, ((PassThroughTransform) transform).firstAffectedCoordinate); + assertEquals(2, ((PassThroughTransform) transform).numTrailingCoordinates); + isInverseTransformSupported = false; + verifyTransform(subTransform, 1); + /* + * Now test with non-consecutive coordinates, except for the `PseudoTransform` part + * which must still be in consecutive coordinates. We add a linear transform before + * for making the work a little bit harder. + */ + bitset.clear(); + bitset.set(1, true); + bitset.set(3, 5, true); // The inseparable `PseudoTransform` part. + bitset.set(6, true); + bitset.set(9, true); + subTransform = MathTransforms.concatenate(MathTransforms.scale(4, 3, 7, 5, -6), transform); + transform = PassThroughTransform.create(bitset, subTransform, 10, null); + verifyTransform( + // _____________[0]_________[1]-____[2]____[3]_______[4] Dimension index in sub-transform. + new double[] {2, 1, -1, 0.2, 0.1, 9, 2, 8, 4, -1}, + new double[] {2, 4, -1, 1600.6, 2700.7, 9, 10, 8, 4, 6}); + } }