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 d1a38eebf9fcff0d2cf50cbc13f6b313208695b6 Author: Martin Desruisseaux <[email protected]> AuthorDate: Mon Jun 16 17:35:03 2025 +0200 Fix a `NonInvertibleTransformException` in calls to `GridGeometry.createTransformTo(…)` when the number of dimensions is not the same in source and target and some dimensions are non-linear. --- .../coverage/grid/CoordinateOperationFinder.java | 116 +++++++++----- .../sis/coverage/grid/GridDerivationTest.java | 28 ++++ .../apache/sis/coverage/grid/GridGeometryTest.java | 61 ++++++-- .../apache/sis/referencing/internal/Resources.java | 5 + .../sis/referencing/internal/Resources.properties | 1 + .../referencing/internal/Resources_fr.properties | 1 + .../apache/sis/referencing/operation/CRSPair.java | 4 +- .../operation/CoordinateOperationFinder.java | 27 ++-- .../referencing/operation/SubOperationInfo.java | 174 +++++++++++++-------- .../operation/transform/MathTransforms.java | 36 ++++- 10 files changed, 328 insertions(+), 125 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java index 52be84d555..ce1971a943 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java @@ -106,6 +106,14 @@ final class CoordinateOperationFinder implements Supplier<double[]> { */ private CoordinateOperation changeOfCRS; + /** + * The source <abbr>CRS</abbr> of the {@link #changeOfCRS} operation. + * May be non-null when {@link #changeOfCRS} is null if it was determined in an indirect way. + * + * @see #getSourceCRS() + */ + private CoordinateReferenceSystem sourceCRS; + /** * Whether the {@link #changeOfCRS} operation has been determined. * Note that result of determining that operation may be {@code null}, which is why we need this flag. @@ -286,6 +294,16 @@ final class CoordinateOperationFinder implements Supplier<double[]> { isWraparoundDisabled = true; } + /** + * Returns the source <abbr>CRS</abbr> of the {@link #changeOfCRS} transform. + */ + private CoordinateReferenceSystem getSourceCRS() throws FactoryException, TransformException { + if (sourceCRS == null) { + sourceCRS = changeOfCRS().getSourceCRS(); + } + return sourceCRS; + } + /** * Returns the target of the "corner to CRS" transform. * May be {@code null} if the neither the source and target grid geometry define a CRS. @@ -309,31 +327,9 @@ final class CoordinateOperationFinder implements Supplier<double[]> { */ private CoordinateOperation changeOfCRS() throws FactoryException, TransformException { if (!knowChangeOfCRS) { - final Envelope sourceEnvelope = source.envelope; - final Envelope targetEnvelope = target.envelope; try { CoordinateOperations.CONSTANT_COORDINATES.set(this); - if (sourceEnvelope != null && targetEnvelope != null) { - changeOfCRS = Envelopes.findOperation(sourceEnvelope, targetEnvelope); - } - if (changeOfCRS == null && source.isDefined(GridGeometry.CRS) && target.isDefined(GridGeometry.CRS)) { - final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem(); - /* - * Unconditionally create operation even if CRS are the same. A non-null operation trig - * the check for wraparound axes, which is necessary even if the transform is identity. - */ - DefaultGeographicBoundingBox areaOfInterest = null; - if (sourceEnvelope != null || targetEnvelope != null) try { - areaOfInterest = new DefaultGeographicBoundingBox(); - areaOfInterest.setBounds(targetEnvelope != null ? targetEnvelope : sourceEnvelope); - } catch (TransformException e) { - areaOfInterest = null; - recoverableException("changeOfCRS", e); - } - changeOfCRS = CRS.findOperation(sourceCRS, target.getCoordinateReferenceSystem(), areaOfInterest); - } - } catch (BackingStoreException e) { // May be thrown by getConstantCoordinates(). - throw e.unwrapOrRethrow(TransformException.class); + changeOfCRS = changeOfCRS(source, target); } finally { CoordinateOperations.CONSTANT_COORDINATES.remove(); } @@ -342,6 +338,47 @@ final class CoordinateOperationFinder implements Supplier<double[]> { return changeOfCRS; } + /** + * Computes the change of <abbr>CRS</abbr> between the given pair of grid geometries. + * + * @param source the grid geometry which is the source of the coordinate operation to find. + * @param target the grid geometry which is the target of the coordinate operation to find. + * @return coordinate operation from source to target CRS, or {@code null} if none. + * @throws FactoryException if no operation can be found between the source and target CRS. + * @throws TransformException if some coordinates cannot be transformed to the specified target. + */ + private static CoordinateOperation changeOfCRS(final GridGeometry source, final GridGeometry target) + throws FactoryException, TransformException + { + CoordinateOperation changeOfCRS = null; + final Envelope sourceEnvelope = source.envelope; + final Envelope targetEnvelope = target.envelope; + try { + if (sourceEnvelope != null && targetEnvelope != null) { + changeOfCRS = Envelopes.findOperation(sourceEnvelope, targetEnvelope); + } + if (changeOfCRS == null && source.isDefined(GridGeometry.CRS) && target.isDefined(GridGeometry.CRS)) { + final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem(); + /* + * Unconditionally create operation even if CRS are the same. A non-null operation trig + * the check for wraparound axes, which is necessary even if the transform is identity. + */ + DefaultGeographicBoundingBox areaOfInterest = null; + if (sourceEnvelope != null || targetEnvelope != null) try { + areaOfInterest = new DefaultGeographicBoundingBox(); + areaOfInterest.setBounds(targetEnvelope != null ? targetEnvelope : sourceEnvelope); + } catch (TransformException e) { + areaOfInterest = null; + recoverableException("changeOfCRS", e); + } + changeOfCRS = CRS.findOperation(sourceCRS, target.getCoordinateReferenceSystem(), areaOfInterest); + } + } catch (BackingStoreException e) { // May be thrown by getConstantCoordinates(). + throw e.unwrapOrRethrow(TransformException.class); + } + return changeOfCRS; + } + /** * Computes the transform from “grid coordinates of the source” to “grid coordinates of the target”. * This is a concatenation of {@link #gridToCRS()} with target "CRS to grid" transform. @@ -410,8 +447,8 @@ apply: if (forwardChangeOfCRS == null) { targetMedian = sourceMedian; sourceMedian = null; } - final WraparoundApplicator ap = new WraparoundApplicator(sourceMedian, - targetMedian, changeOfCRS.getTargetCRS().getCoordinateSystem()); + final var ap = new WraparoundApplicator(sourceMedian, targetMedian, + changeOfCRS.getTargetCRS().getCoordinateSystem()); forwardChangeOfCRS = ap.forDomainOfUse(forwardChangeOfCRS); } } @@ -432,13 +469,23 @@ apply: if (forwardChangeOfCRS == null) { */ final MathTransform inverse() throws FactoryException, TransformException { final MathTransform sourceCrsToGrid = source.getGridToCRS(anchor).inverse(); - @SuppressWarnings("LocalVariableHidesMemberVariable") - final CoordinateOperation changeOfCRS = changeOfCRS(); - if (changeOfCRS == null) { - return sourceCrsToGrid; - } if (inverseChangeOfCRS == null) { - inverseChangeOfCRS = changeOfCRS.getMathTransform().inverse(); + if (knowChangeOfCRS) { + @SuppressWarnings("LocalVariableHidesMemberVariable") + final CoordinateOperation changeOfCRS = changeOfCRS(); + if (changeOfCRS != null) { + inverseChangeOfCRS = changeOfCRS.getMathTransform().inverse(); + } + } else { + final CoordinateOperation inverse = changeOfCRS(target, source); + if (inverse != null) { + sourceCRS = inverse.getTargetCRS(); + inverseChangeOfCRS = inverse.getMathTransform(); + } + } + if (inverseChangeOfCRS == null) { + return sourceCrsToGrid; + } if (!isWraparoundDisabled) { isWraparoundApplied = false; if (!isWraparoundNeedVerified) { @@ -605,8 +652,7 @@ apply: if (forwardChangeOfCRS == null) { } if (sourceMedian != null) { final MathTransform inverseNoWrap = inverseChangeOfCRS; - final WraparoundApplicator ap = new WraparoundApplicator(targetMedian, - sourceMedian, changeOfCRS().getSourceCRS().getCoordinateSystem()); + final var ap = new WraparoundApplicator(targetMedian, sourceMedian, getSourceCRS().getCoordinateSystem()); inverseChangeOfCRS = ap.forDomainOfUse(inverseNoWrap); if (inverseChangeOfCRS != inverseNoWrap) { crsToGrid = MathTransforms.concatenate(inverseChangeOfCRS, sourceCrsToGrid); @@ -671,8 +717,8 @@ apply: if (forwardChangeOfCRS == null) { * Invoked when the target CRS has some dimensions that the source CRS does not have. * For example, this is invoked during the conversion from (<var>x</var>, <var>y</var>) * coordinates to (<var>x</var>, <var>y</var>, <var>t</var>). If constant values can - * be given to the missing dimensions, than those values are returned. Otherwise this - * method returns {@code null}. + * be given to the missing dimensions, then those values are returned. + * Otherwise this method returns {@code null}. * * <p>The returned array has a length equals to the number of dimensions in the target CRS. * Only coordinates in dimensions without source (<var>t</var> in the above example) will be used. diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridDerivationTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridDerivationTest.java index d6055afc6f..86fe67d11e 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridDerivationTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridDerivationTest.java @@ -357,6 +357,34 @@ public final class GridDerivationTest extends TestCase { assertExtentEquals(new long[] {15, 3, 0}, new long[] {49, 9, 5}, grid.getExtent()); } + /** + * Tests {@link GridDerivation#subgrid(GridGeometry)} using a grid with less dimensions + * than the source grid geometry. + * + * @see <a href="https://issues.apache.org/jira/browse/SIS-610">SIS-610</a> + */ + @Test + public void testSubgridWithLessDimensions() { + var envelope = new GeneralEnvelope(HardCodedCRS.WGS84_4D); + envelope.setRange(0, 10, 20); + envelope.setRange(1, 30, 40); + envelope.setRange(2, 2, 4); + envelope.setRange(3, 3, 6); + var env2D = new Envelope2D(HardCodedCRS.WGS84, 10, 30, 10, 10); + var extent = new GridExtent(null, null, new long[] {2, 2, 1, 1}, false); + var grid = new GridGeometry(extent, envelope, GridOrientation.DISPLAY); + var aoi = new GridGeometry(new GridExtent(2, 2), env2D, GridOrientation.DISPLAY); + GridGeometry slice = grid.derive().subgrid(aoi).build(); + Matrix gridToCRS = MathTransforms.getMatrix(slice.getGridToCRS(PixelInCell.CELL_CORNER)); + Matrix expected = Matrices.create(5, 5, new double[] { + 5, 0, 0, 0, 10, + 0, -5, 0, 0, 40, + 0, 0, 2, 0, 2, + 0, 0, 0, 3, 3, + 0, 0, 0, 0, 1}); + assertMatrixEquals(expected, gridToCRS, STRICT, "gridToCRS"); + } + /** * Tests {@link GridDerivation#subgrid(Envelope, double...)} on a grid using a polar projection. * The test also uses a geographic envelope with more dimensions than the source grid geometry. diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java index 603e8c6259..b423c0b2c2 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java @@ -277,7 +277,8 @@ public final class GridGeometryTest extends TestCase { 1, 0, 0.5, 0, 1, 0.5, 0, 0, 1)); - final GridGeometry grid = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, null); + + final var grid = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, null); assertTrue(grid.getGridToCRS(PixelInCell.CELL_CORNER).isIdentity(), "gridToCRS.isIdentity"); verifyGridToCRS(grid); } @@ -324,7 +325,8 @@ public final class GridGeometryTest extends TestCase { 0, 0.5, -90, 0.5, 0, -180, 0, 0, 1)); - final GridGeometry grid = new GridGeometry(PixelInCell.CELL_CORNER, gridToCRS, envelope, GridRoundingMode.NEAREST); + + final var grid = new GridGeometry(PixelInCell.CELL_CORNER, gridToCRS, envelope, GridRoundingMode.NEAREST); assertExtentEquals( new long[] {370, 40}, new long[] {389, 339}, grid.getExtent()); @@ -362,7 +364,7 @@ public final class GridGeometryTest extends TestCase { * Simplest case: no axis flip. * Verification: y = 2 × −25 + 40 = −10 (the minimum value declared in envelope). */ - GridGeometry grid = new GridGeometry(extent, aoi, GridOrientation.HOMOTHETY); + var grid = new GridGeometry(extent, aoi, GridOrientation.HOMOTHETY); Matrix matrix = MathTransforms.getMatrix(grid.getGridToCRS(PixelInCell.CELL_CORNER)); assertMatrixEquals(new Matrix3(0.5, 0, 50, 0, 2, 40, @@ -421,7 +423,7 @@ public final class GridGeometryTest extends TestCase { * Same case as the one tested by `testFromExtentAndEnvelope()`, * but with axis order swapped. */ - GridGeometry grid = new GridGeometry(extent, aoi, GridOrientation.DISPLAY); + var grid = new GridGeometry(extent, aoi, GridOrientation.DISPLAY); Matrix matrix = MathTransforms.getMatrix(grid.getGridToCRS(PixelInCell.CELL_CORNER)); assertMatrixEquals(new Matrix3(0, 0.5, 50, -2, 0, 20, @@ -528,23 +530,23 @@ public final class GridGeometryTest extends TestCase { public void testUpsample() { final GridGeometry grid; { // Source grid - final GridExtent extent = new GridExtent(null, new long[] {10,-8}, new long[] {100, 50}, false); - final Matrix3 mat = new Matrix3( + var extent = new GridExtent(null, new long[] {10,-8}, new long[] {100, 50}, false); + var mat = new Matrix3( 1, 0, 10, 0, -2, 50, 0, 0, 1); - final MathTransform gridToCRS = MathTransforms.linear(mat); + MathTransform gridToCRS = MathTransforms.linear(mat); grid = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, HardCodedCRS.CARTESIAN_2D); } final GridGeometry upsampled = grid.upsample(4, 4); final GridGeometry expected; { // Expected grid - GridExtent extent = new GridExtent(null, new long[] {40,-32}, new long[] {400, 200}, false); - final Matrix3 mat = new Matrix3( + var extent = new GridExtent(null, new long[] {40,-32}, new long[] {400, 200}, false); + var mat = new Matrix3( 0.25, 0, 9.625, 0, -0.5, 50.750, 0, 0, 1); - final MathTransform gridToCRS = MathTransforms.linear(mat); + MathTransform gridToCRS = MathTransforms.linear(mat); expected = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, HardCodedCRS.CARTESIAN_2D); } assertSame(grid.getEnvelope(), upsampled.getEnvelope(), "envelope"); @@ -559,7 +561,7 @@ public final class GridGeometryTest extends TestCase { */ @Test public void testShiftGrid() { - GridGeometry grid = new GridGeometry( + final var grid = new GridGeometry( new GridExtent(17, 10), PixelInCell.CELL_CENTER, MathTransforms.linear(new Matrix3( @@ -796,6 +798,43 @@ public final class GridGeometryTest extends TestCase { MathTransforms.getMatrix(tr), STRICT, "createTransformTo"); } + /** + * Tests {@link GridGeometry#createTransformTo(GridGeometry, PixelInCell)} with a change + * in the number of dimensions. One of the dimensions is non-linear. + * + * @throws TransformException if the transform cannot be computed. + */ + @Test + public void testCreateTransformWithDimensionChange() throws TransformException { + final var source = new GridGeometry( + new GridExtent(null, null, new long[] {17, 10, 6}, false), + PixelInCell.CELL_CENTER, + MathTransforms.compound( + MathTransforms.linear(new Matrix3( + 1, 0, -7.0, + 0, -1, 50.0, + 0, 0, 1)), + MathTransforms.interpolate(null, new double[] {3, 4, 9})), + HardCodedCRS.WGS84_WITH_TIME); + + final var target = new GridGeometry( + new GridExtent(200, 300), + PixelInCell.CELL_CENTER, + MathTransforms.linear(new Matrix3( + -0.05, 0, 53.0, + 0, 0.1, -8.0, + 0, 0, 1)), + HardCodedCRS.WGS84_LATITUDE_FIRST); + + final Matrix expected = Matrices.create(3, 4, new double[] { + 0, 20, 0, 60, + 10, 0, 0, 10, + 0, 0, 0, 1}); + + final MathTransform tr = source.createTransformTo(target, PixelInCell.CELL_CENTER); + assertMatrixEquals(expected, MathTransforms.getMatrix(tr), STRICT, "createTransformTo"); + } + /** * Tests {@link GridGeometry#contains(GridGeometry)} and {@link GridGeometry#intersects(GridGeometry)}. */ diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java index 96af716fd4..34eeeae90d 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java @@ -187,6 +187,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short ConformanceMeansDatumShift = 11; + /** + * A constant value is required for the coordinate at index {0}. + */ + public static final short ConstantCoordinateValueRequired_1 = 109; + /** * This parameter is shown for completeness, but should never have a value different than {0} * for this projection. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties index edb2538662..74149a92a1 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties @@ -72,6 +72,7 @@ CanNotTransformEnvelopeToGeodetic = Cannot transform envelope to a geodetic refe CanNotTransformGeometry = Cannot transform the given geometry. CanNotUseGeodeticParameters_2 = Cannot use the {0} geodetic parameters. Caused by: {1} ColinearAxisDirections_2 = Axis directions {0} and {1} are colinear. +ConstantCoordinateValueRequired_1 = A constant value is required for the coordinate at index {0}. CoordinateOperationNotFound_2 = Coordinate operation from system \u201c{0}\u201d to \u201c{1}\u201d has not been found. DatumChangesDirectory_1 = Datum shift files are searched in the \u201c{0}\u201d directory. DatumOriginShallBeDate = Origin of temporal datum shall be a date. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties index 94495000ef..e63ca97752 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties @@ -77,6 +77,7 @@ CanNotTransformEnvelopeToGeodetic = Ne peut pas transformer l\u2019enveloppe ver CanNotTransformGeometry = Ne peut pas transformer la g\u00e9om\u00e9trie donn\u00e9e. CanNotUseGeodeticParameters_2 = Ne peut pas utiliser les param\u00e8tres g\u00e9od\u00e9siques {0}. La cause est\u202f: {1} ColinearAxisDirections_2 = Les directions d\u2019axes {0} et {1} sont colin\u00e9aires. +ConstantCoordinateValueRequired_1 = Une valeur constante est requise pour la coordonn\u00e9e \u00e0 l\u2019index {0}. CoordinateOperationNotFound_2 = L\u2019op\u00e9ration sur les coordonn\u00e9es du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb vers \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e. DatumChangesDirectory_1 = Les fichiers de changements de r\u00e9f\u00e9rentiel sont cherch\u00e9s dans le dossier \u00ab\u202f{0}\u202f\u00bb. DatumOriginShallBeDate = L\u2019origine d\u2019un r\u00e9f\u00e9rentiel temporel doit \u00eatre une date. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java index 0e46001355..3df3ecb118 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java @@ -35,7 +35,7 @@ import org.apache.sis.util.privy.Strings; * * @author Martin Desruisseaux (Geomatys) */ -final class CRSPair { +final class CRSPair extends org.apache.sis.pending.jdk.Record { /** * The source and target CRS. */ @@ -70,7 +70,7 @@ final class CRSPair { @Override public boolean equals(final Object object) { if (object instanceof CRSPair) { - final CRSPair that = (CRSPair) object; + final var that = (CRSPair) object; return Objects.equals(this.sourceCRS, that.sourceCRS) && Objects.equals(this.targetCRS, that.targetCRS); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java index 2eb7c5a3fa..353eaaa8db 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java @@ -809,6 +809,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { final SubOperationInfo[] infos; try { infos = SubOperationInfo.createSteps(this, sourceComponents, targetComponents); + } catch (NoninvertibleTransformException e) { + throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), e); } catch (TransformException e) { throw new FactoryException(notFoundMessage(sourceCRS, targetCRS), e); } @@ -845,10 +847,10 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { operation = createFromAffineTransform(AXIS_CHANGES, sourceCRS, stepSourceCRS, null, select); } /* - * For each sub-operation, create a PassThroughOperation for the (stepSourceCRS → stepTargetCRS) operation. - * Each source CRS inside this loop will be for dimensions at indices [startAtDimension … endAtDimension-1]. + * For each sub-operation, create a `PassThroughOperation` for the (`stepSourceCRS` → `stepTargetCRS`) operation. + * Each source CRS in the loop will be for dimensions at indices [`firstAffectedCoordinate` … `endAtDimension`-1]. * Note that those indices are not necessarily the same as the indices in the fields of the same name in - * SubOperationInfo, because those indices are not relative to the same CompoundCRS. + * `SubOperationInfo`, because those indices are not relative to the same `CompoundCRS`. */ int endAtDimension = 0; int remainingSourceDimensions = select.getNumRow() - 1; @@ -874,13 +876,18 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { stepTargetCRS = createCompoundCRS(target, stepComponents); } int delta = source.getCoordinateSystem().getDimension(); - final int startAtDimension = endAtDimension; + final int firstAffectedCoordinate = endAtDimension; endAtDimension += delta; final int numTrailingCoordinates = remainingSourceDimensions - endAtDimension; CoordinateOperation subOperation = info.operation; - if ((startAtDimension | numTrailingCoordinates) != 0) { - subOperation = new DefaultPassThroughOperation(IdentifiedObjects.getProperties(subOperation), - stepSourceCRS, stepTargetCRS, subOperation, startAtDimension, numTrailingCoordinates); + if ((firstAffectedCoordinate | numTrailingCoordinates) != 0) { + subOperation = new DefaultPassThroughOperation( + IdentifiedObjects.getProperties(subOperation), + stepSourceCRS, + stepTargetCRS, + subOperation, + firstAffectedCoordinate, + numTrailingCoordinates); } /* * Concatenate the operation with the ones we have found so far, and use the current `stepTargetCRS` @@ -899,11 +906,13 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { * source dimensions, add those constants as the last step. It never occur except is some particular * contexts like when computing a transform between two `GridGeometry` instances. */ - if (stepComponents.length < infos.length) { - final Matrix m = SubOperationInfo.createConstantOperation(infos, stepComponents.length, + if (stepComponents.length < infos.length) try { + final Matrix m = SubOperationInfo.createConstantOperation(infos, stepSourceCRS.getCoordinateSystem().getDimension(), targetCRS.getCoordinateSystem().getDimension()); operation = concatenate(operation, createFromAffineTransform(CONSTANTS, stepSourceCRS, targetCRS, null, m)); + } catch (TransformException e) { + throw new FactoryException(notFoundMessage(sourceCRS, targetCRS), e); } return asList(operation); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java index 277276cd67..75f660aebb 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java @@ -18,12 +18,16 @@ package org.apache.sis.referencing.operation; import java.util.List; import org.opengis.referencing.crs.*; +import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.OperationNotFoundException; +import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.TransformException; import org.opengis.util.FactoryException; +import org.apache.sis.referencing.internal.Resources; import org.apache.sis.referencing.operation.matrix.Matrices; import org.apache.sis.referencing.operation.matrix.MatrixSIS; +import org.apache.sis.referencing.operation.transform.MathTransforms; // Specific to the geoapi-4.0 branch: import org.apache.sis.referencing.crs.DefaultImageCRS; @@ -82,32 +86,29 @@ final class SubOperationInfo { /** * The constant values to store in target coordinates, or {@code null} if none. This array is usually null. - * It may be non-null if no source CRS component has been found for a target CRS component. For example, if - * the source CRS has (<var>x</var>, <var>y</var>) axes and the target CRS has (<var>x</var>, <var>y</var>, - * <var>t</var>) axes, then this array may be set to a non-null value for specifying the <var>t</var> value. - * The array length is the number of dimensions in the full (usually compound) target CRS, but only the - * coordinate values for sourceless dimensions are used. Other coordinates are ignored and can be NaN. + * It may be non-null if no source CRS component has been found for a target CRS component. * Exactly one of {@link #operation} or {@link #constants} shall be non-null. * + * <p>The array length is the number of dimensions in the full (usually compound) target CRS, but only the + * coordinate values for sourceless dimensions are used. Other coordinates are ignored and can be NaN.</p> + * + * <h4>Example</h4> + * If the source <abbr>CRS</abbr> has (<var>x</var>, <var>y</var>) axes and the target <abbr>CRS</abbr> + * has (<var>x</var>, <var>y</var>, <var>t</var>) axes, then this array may be set to a non-null value + * for specifying the <var>t</var> value. + * * @see CoordinateOperationContext#getConstantCoordinates() */ private final double[] constants; /** * The first dimension (inclusive) and the last dimension (exclusive) where the {@link SingleCRS} starts/ends - * in the full (usually compound) CRS. It may be an index in source dimensions or in target dimensions, - * depending on the following rules: - * - * <ul> - * <li>If {@link #operation} is non-null, then this is an index in the <em>source</em> dimensions. - * This is the normal case.</li> - * <li>Otherwise the operation is sourceless with coordinate results described by the {@link #constants} array. - * Since there is no source, the index become an index in target dimensions instead of source dimensions.</li> - * </ul> + * in the full (usually compound) CRS. * * @see #sourceToSelected(int, SubOperationInfo[]) */ - private final int startAtDimension, endAtDimension; + private final int sourceLowerDimension, sourceUpperDimension, + targetLowerDimension, targetUpperDimension; /** * Index of this instance in the array of {@code SubOperationInfo} instances, @@ -119,13 +120,17 @@ final class SubOperationInfo { * Creates a new instance wrapping the given coordinate operation or coordinate constants. * Exactly one of {@code operation} or {@code constants} shall be non-null. */ - private SubOperationInfo(final int targetComponentIndex, final CoordinateOperation operation, - final double[] constants, final int startAtDimension, final int endAtDimension) + private SubOperationInfo(final CoordinateOperation operation, final double[] constants, + final int sourceLowerDimension, final int sourceUpperDimension, + final int targetLowerDimension, final int targetUpperDimension, + final int targetComponentIndex) { this.operation = operation; this.constants = constants; - this.startAtDimension = startAtDimension; - this.endAtDimension = endAtDimension; + this.sourceLowerDimension = sourceLowerDimension; + this.sourceUpperDimension = sourceUpperDimension; + this.targetLowerDimension = targetLowerDimension; + this.targetUpperDimension = targetUpperDimension; this.targetComponentIndex = targetComponentIndex; assert (operation == null) != (constants == null); } @@ -140,29 +145,26 @@ final class SubOperationInfo { * @param targets all components of the target CRS. * @return information about each coordinate operation from a source CRS to a target CRS, or {@code null}. * @throws FactoryException if an error occurred while grabbing a coordinate operation. - * @throws TransformException if an error occurred while computing the sourceless coordinate constants. + * @throws TransformException if an error occurred while computing a coordinate value for unmatched dimension. + * @throws NoninvertibleTransformException if the default coordinate value for a unmatched dimension is NaN. */ static SubOperationInfo[] createSteps(final CoordinateOperationFinder caller, final List<? extends SingleCRS> sources, final List<? extends SingleCRS> targets) throws FactoryException, TransformException { - final SubOperationInfo[] infos = new SubOperationInfo[targets.size()]; - final boolean[] sourceComponentIsUsed = new boolean[sources.size()]; + final var infos = new SubOperationInfo[targets.size()]; + final var sourceComponentIsUsed = new boolean[sources.size()]; /* * Iterate over target CRS because all of them must have an operation. * One the other hand, source CRS can be used zero or one (not two) time. - * - * Note: the loops on source CRS and on target CRS follow the same pattern. - * The first 6 lines of code are the same with only `target` replaced - * by `source`. */ - int targetStartAtDimension; - int targetEndAtDimension = 0; + int targetLowerDimension; + int targetUpperDimension = 0; next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; targetComponentIndex++) { final SingleCRS target = targets.get(targetComponentIndex); - targetStartAtDimension = targetEndAtDimension; - targetEndAtDimension += target.getCoordinateSystem().getDimension(); + targetLowerDimension = targetUpperDimension; + targetUpperDimension += target.getCoordinateSystem().getDimension(); final Class<?> targetType = type(target); OperationNotFoundException failure = null; @@ -171,17 +173,18 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; * Some sources may be left unused after this method completion. Check only * sources that may be compatible according the `COMPATIBLE_TYPES` array. */ - int sourceStartAtDimension; - int sourceEndAtDimension = 0; + int sourceLowerDimension; + int sourceUpperDimension = 0; for (int sourceComponentIndex = 0; sourceComponentIndex < sourceComponentIsUsed.length; sourceComponentIndex++) { final SingleCRS source = sources.get(sourceComponentIndex); - sourceStartAtDimension = sourceEndAtDimension; - sourceEndAtDimension += source.getCoordinateSystem().getDimension(); + sourceLowerDimension = sourceUpperDimension; + sourceUpperDimension += source.getCoordinateSystem().getDimension(); if (!sourceComponentIsUsed[sourceComponentIndex]) { - for (final Class<?>[] sourceTypes : COMPATIBLE_TYPES) { - if (sourceTypes[0].isAssignableFrom(targetType)) { - for (final Class<?> sourceType : sourceTypes) { - if (sourceType.isAssignableFrom(type(source))) { + final Class<?> sourceType = type(source); + for (final Class<?>[] compatibleTypes : COMPATIBLE_TYPES) { + if (compatibleTypes[0].isAssignableFrom(targetType)) { + for (final Class<?> compatibleType : compatibleTypes) { + if (compatibleType.isAssignableFrom(sourceType)) { final CoordinateOperation operation; try { operation = caller.createOperation(source, target); @@ -194,8 +197,8 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; continue; } /* - * Found an operation. Exclude the source component from the list because each source - * should be used at most once by SourceComponent. Note that the same source may still + * Found an operation. Exclude the source component from the list because each source + * should be used at most once by `SubOperationInfo`. Note that the same source may * be used again in another context if that source is also an interpolation CRS. * * EXAMPLE: consider a coordinate operation from (GeodeticCRS₁, VerticalCRS₁) source @@ -204,13 +207,16 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; * to VerticalCRS₂. But the operation on vertical coordinates may need GeodeticCRS₁ * for doing its work, so GeodeticCRS₁ is needed twice. However, when needed for the * vertical coordinate operation, the GeodeticCRS₁ is used as an "interpolation CRS". - * Interpolation CRS are handled in other code paths; it is not the business of this - * SourceComponent class to care about them. From the point of view of this class, + * Interpolation CRS are handled in other code paths, it is not the business of this + * `SubOperationInfo` class to care about them. From the point of view of this class, * GeodeticCRS₁ is used only once. */ sourceComponentIsUsed[sourceComponentIndex] = true; - infos[targetComponentIndex] = new SubOperationInfo(targetComponentIndex, - operation, null, sourceStartAtDimension, sourceEndAtDimension); + infos[targetComponentIndex] = new SubOperationInfo( + operation, null, + sourceLowerDimension, sourceUpperDimension, + targetLowerDimension, targetUpperDimension, + targetComponentIndex); if (failure != null) { CoordinateOperationRegistry.recoverableException("decompose", failure); @@ -227,21 +233,27 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; } /* * If we reach this point, we have not been able to find a source CRS that we can map to the target CRS. - * Usually this is fatal; returning null will instruct the caller to throw `OperationNotFoundException`. + * Usually this is fatal, returning null will instruct the caller to throw `OperationNotFoundException`. * However, in some contexts (e.g. when searching for an operation between two `GridGeometry` instances) * it is possible to assign a constant value to the target coordinates. Those values cannot be guessed * by `org.apache.sis.referencing`; they must be provided by caller. If such constants are specified, * then we will try to apply them. */ final double[] constants = CoordinateOperationContext.getConstantCoordinates(); - if (constants == null || constants.length < targetEndAtDimension) { + if (constants == null || constants.length < targetUpperDimension) { return null; } - for (int i = targetStartAtDimension; i < targetEndAtDimension; i++) { - if (Double.isNaN(constants[i])) return null; + for (int i = targetLowerDimension; i < targetUpperDimension; i++) { + if (Double.isNaN(constants[i])) { + throw new NoninvertibleTransformException(caller.resources() + .getString(Resources.Keys.ConstantCoordinateValueRequired_1, i)); + } } - infos[targetComponentIndex] = new SubOperationInfo(targetComponentIndex, - null, constants, targetStartAtDimension, targetEndAtDimension); + infos[targetComponentIndex] = new SubOperationInfo( + null, constants, + sourceUpperDimension, sourceUpperDimension, + targetLowerDimension, targetUpperDimension, + targetComponentIndex); } return infos; } @@ -249,8 +261,8 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; /** * Returns the source CRS of given operations. This method modifies the given array in-place by moving all * sourceless operations last. Then an array is returned with the source CRS of only ordinary operations. - * Each CRS at index <var>i</var> in the returned array is the component from {@link #startAtDimension} - * inclusive to {@link #endAtDimension} exclusive in the complete (usually compound) source CRS analyzed + * Each CRS at index <var>i</var> in the returned array is the component from {@link #sourceLowerDimension} + * inclusive to {@link #sourceUpperDimension} exclusive in the complete (usually compound) source CRS analyzed * by {@link CoordinateOperationFinder}. * * @param selected all operations from source to target {@link CompoundCRS}. @@ -267,7 +279,7 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; n--; } } - final CoordinateReferenceSystem[] stepComponents = new CoordinateReferenceSystem[n]; + final var stepComponents = new CoordinateReferenceSystem[n]; for (int i=0; i<n; i++) { stepComponents[i] = selected[i].operation.getSourceCRS(); } @@ -300,7 +312,7 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; * the source CRS are not necessarily picked in the same order as they appear in the list. * * <h4>Example</h4> - * if the source CRS has (<var>x</var>, <var>y</var>, <var>t</var>) coordinates and the target CRS has + * If the source CRS has (<var>x</var>, <var>y</var>, <var>t</var>) coordinates and the target CRS has * (<var>t</var>, <var>x</var>, <var>y</var>) coordinates with some operation applied on <var>x</var> * and <var>y</var>, then the operations will be applied in that order: * @@ -332,13 +344,14 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; int selectedDimensions = 0; for (final SubOperationInfo component : selected) { if (component.operation == null) break; - selectedDimensions += component.endAtDimension - component.startAtDimension; + selectedDimensions += component.sourceUpperDimension - component.sourceLowerDimension; } final MatrixSIS select = Matrices.createZero(selectedDimensions + 1, sourceDimensions + 1); select.setElement(selectedDimensions, sourceDimensions, 1); int j = 0; for (final SubOperationInfo component : selected) { - for (int i=component.startAtDimension; i<component.endAtDimension; i++) { + if (component.operation == null) break; + for (int i = component.sourceLowerDimension; i < component.sourceUpperDimension; i++) { select.setElement(j++, i, 1); } } @@ -353,26 +366,55 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; } /** - * Returns the matrix of an operation setting some coordinates to a constant values. + * Returns the matrix of an operation setting some coordinates to constant values. * * @param selected all operations from source to target {@link CompoundCRS}. - * @param n index of the first selected operation which describe a constant value. * @param srcDim number of dimensions in the target CRS of previous operation step. * @param tgtDim number of dimensions in the full (usually compound) target CRS. */ - static MatrixSIS createConstantOperation(final SubOperationInfo[] selected, int n, - final int srcDim, final int tgtDim) + static MatrixSIS createConstantOperation(final SubOperationInfo[] selected, final int srcDim, final int tgtDim) + throws TransformException { final boolean[] targetDimensionIsUsed = new boolean[tgtDim]; final MatrixSIS m = Matrices.createZero(tgtDim + 1, srcDim + 1); m.setElement(tgtDim, srcDim, 1); - do { - final SubOperationInfo component = selected[n]; - for (int j = component.startAtDimension; j < component.endAtDimension; j++) { - m.setElement(j, srcDim, component.constants[j]); - targetDimensionIsUsed[j] = true; + for (final SubOperationInfo component : selected) { + if (component.constants != null) { + /* + * Component for which no operation has been found. + * Set all coordinate values of that component to the specified constants + */ + for (int j = component.targetLowerDimension; j < component.targetUpperDimension; j++) { + m.setElement(j, srcDim, component.constants[j]); + targetDimensionIsUsed[j] = true; + } + } else { + /* + * Component for which an operation has been found, but maybe this is the inverse of the + * "Geographic 3D to 2D conversion" (EPSG:9659) which sets the height to `DEFAULT_HEIGHT`. + * If all values in the row are zero, then the coordinate value is unconditionally zero. + * Replace that value (usually the default ellipsoidal height) by the specified constant. + */ + final Matrix last = MathTransforms.getMatrix(MathTransforms.getLastStep(component.operation.getMathTransform())); + if (last != null) { +otherRow: for (int j = last.getNumRow() - 1; --j >= 0;) { // Ignore the last row. + for (int i = last.getNumCol(); --i >= 0;) { + if (last.getElement(j, i) != 0) { + continue otherRow; + } + } + final double[] constants = CoordinateOperationContext.getConstantCoordinates(); + if (constants == null) break; + final int k = component.targetLowerDimension + j; + m.setElement(j, srcDim, constants[k]); + targetDimensionIsUsed[k] = true; + } + } } - } while (++n < selected.length); + } + /* + * All coordinates that have not been set to a constant shall be propagated unchanged (scale factor of 1). + */ for (int i=0,j=0; j<tgtDim; j++) { if (!targetDimensionIsUsed[j]) { m.setElement(j, i++, 1); diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java index 7be5444319..1c5c2429c1 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java @@ -635,8 +635,8 @@ public final class MathTransforms extends Static { * <li>Otherwise returns the given transform in a list of size 1.</li> * </ul> * - * @param transform the transform for which to get the components, or {@code null}. - * @return all single math transforms performed by this concatenated transform. + * @param transform the transform for which to get the steps, or {@code null}. + * @return all steps performed by the given transform. */ public static List<MathTransform> getSteps(final MathTransform transform) { if (transform != null) { @@ -650,6 +650,38 @@ public final class MathTransforms extends Static { } } + /** + * Returns the first step of the given (potentially concatenated) transform. + * Invoking this method is equivalent to invoking {@link #getSteps(MathTransform)} + * and retaining only the first element of the list. + * + * @param transform the transform for which to get the first step, or {@code null}. + * @return the first step performed by the given transform, or {@code null} if the argument was null. + * @since 1.5 + */ + public static MathTransform getFirstStep(MathTransform transform) { + while (transform instanceof ConcatenatedTransform) { + transform = ((ConcatenatedTransform) transform).transform1; + } + return transform; + } + + /** + * Returns the last step of the given (potentially concatenated) transform. + * Invoking this method is equivalent to invoking {@link #getSteps(MathTransform)} + * and retaining only the last element of the list. + * + * @param transform the transform for which to get the last step, or {@code null}. + * @return the last step performed by the given transform, or {@code null} if the argument was null. + * @since 1.5 + */ + public static MathTransform getLastStep(MathTransform transform) { + while (transform instanceof ConcatenatedTransform) { + transform = ((ConcatenatedTransform) transform).transform2; + } + return transform; + } + /** * Returns whether the given object is a linear transform. * This method is defined here for keeping it consistent with {@link #getMatrix(MathTransform)}.
