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 2cebe32d40 Refactor the code for filtering the dimensions of a CRS.
2cebe32d40 is described below

commit 2cebe32d400ba9014befd6918c4feb22847abfb3
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Jan 20 17:47:07 2026 +0100

    Refactor the code for filtering the dimensions of a CRS.
    
    - A new `CRS.selectDimensions(CoordinateReferenceSystem, BitSet, 
SeparationMode)` method is added.
    - A new `GridGeometry.getConstantCoordinates()` method is added, which uses 
the above-cited method.
    - The GeoTIFF `MultiResolutionImage` class uses the above for accepting 2D 
request on 3D data.
---
 .../apache/sis/coverage/grid/DefaultEvaluator.java |   4 +-
 .../apache/sis/coverage/grid/DimensionReducer.java |   7 +-
 .../apache/sis/coverage/grid/GridDerivation.java   |  47 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  96 ++++-
 .../sis/coverage/grid/GridDerivationTest.java      |   5 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  24 ++
 .../sis/geometry/AbstractDirectPosition.java       |  34 +-
 .../apache/sis/geometry/GeneralDirectPosition.java |  39 +-
 .../sis/geometry/ImmutableDirectPosition.java      | 161 +++++++
 .../org/apache/sis/geometry/ImmutableEnvelope.java |  15 +-
 .../main/org/apache/sis/referencing/CRS.java       | 477 +++++++++++++++++----
 .../internal/shared/DirectPositionView.java        |   3 +-
 .../referencing/operation/SubOperationInfo.java    |  10 +-
 .../test/org/apache/sis/referencing/CRSTest.java   |  11 +
 .../sis/storage/geotiff/MultiResolutionImage.java  |  22 +-
 netbeans-project/nbproject/project.xml             |   1 +
 16 files changed, 789 insertions(+), 167 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
index e1198e6489..3662d73a6e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
@@ -626,7 +626,7 @@ next:   while (--numPoints >= 0) {
         MathTransform crsToGrid = 
TranslatedTransform.resolveNaN(gridToCRS.inverse(), gridGeometry);
         if (crs != null) {
             final CoordinateReferenceSystem stepCRS = 
coverage.getCoordinateReferenceSystem();
-            final GeographicBoundingBox areaOfInterest = 
gridGeometry.geographicBBox();
+            final GeographicBoundingBox areaOfInterest = 
gridGeometry.getGeographicExtent().orElse(null);
             try {
                 CoordinateOperation op = CRS.findOperation(crs, stepCRS, 
areaOfInterest);
                 crsToGrid = MathTransforms.concatenate(op.getMathTransform(), 
crsToGrid);
@@ -670,7 +670,7 @@ next:   while (--numPoints >= 0) {
                         } else {
                             final Long value = slice.get(j);
                             if (value == null) {
-                                final GridExtent extent = gridGeometry.extent;
+                                final GridExtent extent = 
gridGeometry.getExtent();
                                 throw new 
FactoryException(Resources.format(Resources.Keys.NoNDimensionalSlice_3,
                                                 crsDim, 
extent.getAxisIdentification(j, j), extent.getSize(j)));
                             }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionReducer.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionReducer.java
index daa2eebcc5..2724153a6e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionReducer.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionReducer.java
@@ -17,6 +17,7 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.Arrays;
+import java.util.BitSet;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
@@ -55,10 +56,10 @@ final class DimensionReducer {
     /**
      * Requests to retain only the axes in the specified <abbr>CRS</abbr> 
dimensions.
      *
-     * @param  dimensions  the <abbr>CRS</abbr> dimensions to keep, or {@code 
null} for keeping them all.
+     * @param  mask  the <abbr>CRS</abbr> dimensions to keep.
      */
-    DimensionReducer(final int... dimensions) {
-        this.dimensions = dimensions;
+    DimensionReducer(final BitSet mask) {
+        dimensions = mask.stream().toArray();
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
index e4840736a9..052b2cf796 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
@@ -35,7 +35,9 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.cs.AxisDirection;
 import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.operation.MissingSourceDimensionsException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
@@ -233,7 +235,7 @@ public class GridDerivation {
      * Indexes of <abbr>CRS</abbr> axes to keep, or {@code null} if no 
filtering will be applied.
      * A non-null value may cause a reduction in the number of dimensions of 
the grid.
      *
-     * @see #project(Set)
+     * @see #selectDimensions(Set)
      */
     private BitSet dimensionsToKeepInCRS;
 
@@ -1202,16 +1204,17 @@ public class GridDerivation {
     }
 
     /**
-     * Requests a projection where only a subset of the <abbr>CRS</abbr> 
dimensions will be kept.
+     * Requests a grid where only a subset of the <abbr>CRS</abbr> dimensions 
will be kept.
      * The real world dimensions to keep are specified by a filter applied on 
the coordinate system axes.
      * This method may reduce the number of dimensions of the grid if, as a 
result of this filtering,
      * some grid dimensions become unrelated to any <abbr>CRS</abbr> axis.
      *
      * <h4>Example</h4>
-     * The following code removes the temporal dimension of a grid geometry:
+     * The following code keeps only the axes having a linear unit of 
measurement such as metres or kilometres:
      *
      * {@snippet lang="java" :
-     *     gridGeometry.derive().project((axis) -> axis.getDirection() != 
AxisDirection.FUTURE).build();
+     *     GridGeometry gg = ...;
+     *     gg = gg.derive().selectDimensions((axis) -> 
Units.isLinear(axis.getUnit())).build();
      *     }
      *
      * @param  filter  a predicate which returns {@code true} for coordinate 
system axes to keep.
@@ -1220,7 +1223,7 @@ public class GridDerivation {
      *
      * @since 1.6
      */
-    public GridDerivation project(final Predicate<CoordinateSystemAxis> 
filter) {
+    public GridDerivation selectDimensions(final 
Predicate<CoordinateSystemAxis> filter) {
         final var dimensions = new BitSet();
         final CoordinateSystem cs = 
base.getCoordinateReferenceSystem().getCoordinateSystem();
         for (int i = cs.getDimension(); --i >= 0;) {
@@ -1236,6 +1239,38 @@ public class GridDerivation {
         return this;
     }
 
+    /**
+     * Requests a grid where some <abbr>CRS</abbr> dimensions are excluded.
+     * The real world dimensions to exclude are specified by the axis 
directions.
+     * This method may reduce the number of dimensions of the grid if, as a 
result of this filtering,
+     * some grid dimensions become unrelated to any <abbr>CRS</abbr> axis.
+     *
+     * <h4>Example</h4>
+     * This method is provided for convenience in the handling of {@link 
MissingSourceDimensionsException}.
+     * A usage pattern can be as below:
+     *
+     * {@snippet lang="java" :
+     *     GridGeometry gg = ...;
+     *     try {
+     *         doSomeStuff(gg);
+     *     } catch (MissingSourceDimensionsException e) {
+     *         gg = 
gg.gridDerivation().excludeDimensions(e.getMissingAxes()).build();
+     *         doSomeStuff(gg);     // Try again.
+     *     }
+     *     }
+     *
+     * @param  exclusion  the dimensions to remove, identified by their axis 
directions.
+     * @return {@code this} for method call chaining.
+     * @throws IncompleteGridGeometryException if the base grid geometry has 
no <abbr>CRS</abbr>.
+     *
+     * @see MissingSourceDimensionsException#getMissingAxes()
+     *
+     * @since 1.6
+     */
+    public GridDerivation excludeDimensions(final Set<AxisDirection> 
exclusion) {
+        return selectDimensions((axis) -> 
!exclusion.contains(axis.getDirection()));
+    }
+
     /**
      * Builds a grid geometry with the configuration specified by the other 
methods in this {@code GridDerivation} class.
      *
@@ -1296,7 +1331,7 @@ public class GridDerivation {
             throw new IllegalGridGeometryException(e, "envelope");
         }
         if (dimensionsToKeepInCRS != null) try {
-            grid = new 
DimensionReducer(dimensionsToKeepInCRS.stream().toArray()).apply(grid);
+            grid = new DimensionReducer(dimensionsToKeepInCRS).apply(grid);
         } catch (FactoryException e) {
             throw new IllegalGridGeometryException(e, "gridToCRS");
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index a4d4d39e06..3aca8de747 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.util.Arrays;
+import java.util.BitSet;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
@@ -30,6 +32,7 @@ import org.opengis.util.FactoryException;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.geometry.Envelope;
+import org.opengis.geometry.DirectPosition;
 import org.opengis.coordinate.CoordinateMetadata;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
@@ -38,8 +41,8 @@ import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.DerivedCRS;
-import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.measure.AngleFormat;
 import org.apache.sis.measure.Latitude;
@@ -48,9 +51,11 @@ import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.geometry.ImmutableDirectPosition;
 import org.apache.sis.coordinate.DefaultCoordinateMetadata;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -85,6 +90,7 @@ import org.apache.sis.io.TableAppender;
 import org.apache.sis.xml.NilObject;
 import org.apache.sis.xml.NilReason;
 import static org.apache.sis.referencing.CRS.findOperation;
+import static org.apache.sis.referencing.CRS.SeparationMode;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.coordinate.MismatchedDimensionException;
@@ -301,6 +307,14 @@ public class GridGeometry implements LenientComparable, 
Serializable {
     @SuppressWarnings("VolatileArrayField")                 // Safe because 
array will not be modified after construction.
     private transient volatile Instant[] timeRange;
 
+    /**
+     * Coordinates that may be considered as constants, or {@code null} if not 
yet computed.
+     * By convention, a position of dimension 0 means that there is no 
constant coordinates.
+     *
+     * @see #getConstantCoordinates()
+     */
+    private transient volatile DirectPosition constantCoordinates;
+
     /**
      * An "empty" grid geometry with no value defined. All getter methods 
invoked on this instance will cause
      * {@link IncompleteGridGeometryException} to be thrown. This instance can 
be used as a place-holder when
@@ -601,7 +615,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * @param  caller     the method where exception occurred.
      * @param  exception  the exception that occurred.
      */
-    static void recoverableException(final String caller, final 
TransformException exception) {
+    static void recoverableException(final String caller, final Exception 
exception) {
         Logging.recoverableException(GridExtent.LOGGER, GridGeometry.class, 
caller, exception);
     }
 
@@ -1120,7 +1134,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * <h4>API note</h4>
      * This method does not throw {@link IncompleteGridGeometryException} 
because the geographic extent
      * may be absent even with a complete grid geometry. Grid geometries are 
not required to have a
-     * spatial component on Earth surface; a raster could be a vertical 
profile for example.
+     * spatial component on Earth surface, since a raster could be a vertical 
profile for example.
      *
      * @return the geographic bounding box in "real world" coordinates.
      */
@@ -1132,7 +1146,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * Returns the {@link #geographicBBox} value or {@code null} if none.
      * This method computes the box when first needed.
      */
-    final GeographicBoundingBox geographicBBox() {
+    private final GeographicBoundingBox geographicBBox() {
         GeographicBoundingBox bbox = geographicBBox;
         if (bbox == null) {
             if (getCoordinateReferenceSystem(envelope) != null && 
!envelope.isAllNaN()) {
@@ -1188,6 +1202,80 @@ public class GridGeometry implements LenientComparable, 
Serializable {
         return times;
     }
 
+    /**
+     * If the envelope has some coordinates that may be considered as 
constant, returns these coordinates.
+     * A constant coordinates is a coordinate where the lower bound is equal 
to the upper bound.
+     * All non-constant coordinates are set to NaN. If this method returns a 
non-empty value,
+     * then it is guaranteed to contain at least one non-NaN value.
+     *
+     * <h4>Coordinate Reference System</h4>
+     * The <abbr>CRS</abbr> of the returned position may be {@link 
#getCoordinateReferenceSystem()}.
+     * But it may also be a subset of the <abbr>CRS</abbr> components 
containing only the components
+     * having at least one non-NaN value. For example, if only the time 
coordinate is constant, then
+     * the <abbr>CRS</abbr> of the returned position may contain only the 
temporal component.
+     *
+     * <h4>Usage</h4>
+     * This is a helper method for computing coordinate operations with grid 
geometries that are,
+     * for example, two-dimensional slices in a three- or four-dimensional 
data cube.
+     * The algorithm should work with any number of dimensions, the 
two-dimensional slice is only an example.
+     * This method is intended to be used with {@link 
CoordinateOperationContext} as below:
+     *
+     * {@snippet lang="java" :
+     *     GridGeometry gg = ...;
+     *     var context = new CoordinateOperationContext();
+     *     gg.getGeographicExtent().ifPresent(context::addAreaOfInterest);
+     *     
gg.getConstantCoordinates().ifPresent(context::setConstantCoordinates);
+     *     CoordinateOperation op = CRS.findOperation(..., 
targetGrid.getCoordinateMetadata(), context);
+     *     }
+     *
+     * @return the constant coordinates, or empty if none.
+     *
+     * @see #getGeographicExtent()
+     * @see CoordinateOperationContext#getConstantCoordinates()
+     * @since 1.6
+     */
+    public Optional<DirectPosition> getConstantCoordinates() {
+        DirectPosition constants = constantCoordinates;
+        if (constants == null && envelope != null) {
+            double[] coordinates = new double[envelope.getDimension()];
+            Arrays.fill(coordinates, Double.NaN);
+            final var selected = new BitSet();
+            for (int i=0; i<coordinates.length; i++) {
+                double lower = envelope.getLower(i);
+                double upper = envelope.getUpper(i);
+                if (Double.isNaN(lower)) lower = upper;
+                if (Double.isNaN(upper)) upper = lower;
+                if (lower == upper) {
+                    coordinates[i] = lower;
+                    selected.set(i);
+                }
+            }
+            CoordinateReferenceSystem crs;
+            if (selected.isEmpty()) {
+                crs = null;
+                coordinates = ArraysExt.EMPTY_DOUBLE;
+            } else {
+                crs = envelope.getCoordinateReferenceSystem();
+                try {
+                    crs = org.apache.sis.referencing.CRS.selectDimensions(crs, 
selected, SeparationMode.WHOLE_UNSEPARABLE);
+                    int count = 0;
+                    for (int i : selected.stream().toArray()) {
+                        coordinates[count++] = coordinates[i];
+                    }
+                    coordinates = ArraysExt.resize(coordinates, count);
+                } catch (FactoryException e) {
+                    recoverableException("getConstantCoordinates", e);
+                }
+            }
+            constants = new ImmutableDirectPosition(crs, coordinates);
+            constantCoordinates = constants;
+        }
+        if (constants != null && constants.getDimension() == 0) {
+            constants = null;
+        }
+        return Optional.ofNullable(constants);
+    }
+
     /**
      * Returns the "real world" coordinates of the cell at indices (0, 0, … 0).
      * The returned coordinates map the {@linkplain PixelInCell#CELL_CORNER 
cell corner}.
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 582e7566ad..98aef0e5c7 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
@@ -17,6 +17,7 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.DoubleStream;
 import java.util.stream.IntStream;
@@ -739,7 +740,7 @@ public final class GridDerivationTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridDerivation#project(Predicate)}.
+     * Tests {@link GridDerivation#selectDimensions(Predicate)}.
      */
     @Test
     public void testProject() {
@@ -751,7 +752,7 @@ public final class GridDerivationTest extends TestCase {
                         0,   0,   2,    3,
                         0,   0,   0,    1)), HardCodedCRS.WGS84_3D);
 
-        GridGeometry projected = grid.derive().project((axis) -> 
axis.getDirection() != AxisDirection.UP).build();
+        GridGeometry projected = 
grid.derive().excludeDimensions(Set.of(AxisDirection.UP)).build();
         assertNotSame(grid, projected);
         assertEquals(2, projected.getDimension());
         assertTrue(CRS.equivalent(HardCodedCRS.WGS84, 
projected.getCoordinateReferenceSystem()));
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 1cb5e7bf9b..0f7c2a0c69 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
@@ -572,6 +572,30 @@ public final class GridGeometryTest extends TestCase {
                 new double[] { 1, -3.0, 0}), envelope);
     }
 
+    /**
+     * Tests {@link GridGeometry#getConstantCoordinates()}.
+     */
+    @Test
+    public void testGetConstantCoordinates() {
+        for (double sy = 0; sy <= 1; sy++) {
+            final var grid = new GridGeometry(
+                    new GridExtent(12, 1),
+                    PixelInCell.CELL_CORNER,
+                    MathTransforms.linear(new Matrix3(
+                        0.25, 0,   -2,
+                        0,    sy,  -3,
+                        0,    0,    1)),
+                    HardCodedCRS.WGS84);
+
+            final var constant = grid.getConstantCoordinates();
+            assertEquals(constant, grid.getConstantCoordinates());      // 
Verify the cache.
+            assertEquals(sy == 0, constant.isPresent());
+            if (sy == 0) {
+                assertArrayEquals(new double[] {Double.NaN, -3}, 
constant.orElseThrow().getCoordinates());
+            }
+        }
+    }
+
     /**
      * Tests {@link GridGeometry#upsample(long...)}.
      */
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/AbstractDirectPosition.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/AbstractDirectPosition.java
index ee791a9b88..7f317671cd 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/AbstractDirectPosition.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/AbstractDirectPosition.java
@@ -58,7 +58,7 @@ import static 
org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
  * serializable, is left to subclasses.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.3
  */
 public abstract class AbstractDirectPosition extends FormattableObject 
implements DirectPosition {
@@ -71,10 +71,10 @@ public abstract class AbstractDirectPosition extends 
FormattableObject implement
     /**
      * Returns the given position as an {@code AbstractDirectPosition} 
instance.
      * If the given position is already an instance of {@code 
AbstractDirectPosition},
-     * then it is returned unchanged. Otherwise the coordinate values and the 
CRS
+     * then it is returned unchanged. Otherwise, the coordinate values and the 
<abbr>CRS</abbr>
      * of the given position are copied in a new position.
      *
-     * @param  position  the position to cast, or {@code null}.
+     * @param  position  the position to cast or copy, or {@code null}.
      * @return the values of the given position as an {@code 
AbstractDirectPosition} instance.
      *
      * @since 1.0
@@ -102,21 +102,25 @@ public abstract class AbstractDirectPosition extends 
FormattableObject implement
      */
     @Override
     public void setCoordinate(int dimension, double value) {
-        throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
getClass()));
+        // Be tolerant if the coordinate is the same for allowing 
`normalize()` to be a no-operation.
+        if (!Numerics.equals(getCoordinate(dimension), value)) {
+            throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
getClass()));
+        }
     }
 
     /**
      * Sets this direct position to the given position. If the given position 
is
      * {@code null}, then all coordinate values are set to {@link Double#NaN 
NaN}.
      *
-     * <p>If this position and the given position have a non-null CRS, then 
the default implementation
-     * requires the CRS to be {@linkplain CRS#equivalent equivalent},
+     * <p>If this position and the given position have a non-null 
<abbr>CRS</abbr>,
+     * then the default implementation requires the <abbr>CRS</abbr> to be 
{@linkplain CRS#equivalent equivalent},
      * otherwise a {@code MismatchedCoordinateMetadataException} is thrown. 
However, subclass may choose
-     * to assign the CRS of this position to the CRS of the given position.</p>
+     * to assign the <abbr>CRS</abbr> of this position to the <abbr>CRS</abbr> 
of the given position.</p>
      *
      * @param  position  the new position, or {@code null}.
      * @throws MismatchedDimensionException if the given position doesn't have 
the expected dimension.
      * @throws MismatchedCoordinateMetadataException if the given position 
doesn't use the expected CRS.
+     * @throws UnsupportedOperationException if this direct position is 
immutable.
      */
     public void setLocation(final DirectPosition position)
             throws MismatchedDimensionException, 
MismatchedCoordinateMetadataException
@@ -196,7 +200,7 @@ public abstract class AbstractDirectPosition extends 
FormattableObject implement
     }
 
     /**
-     * Formats this position in the <i>Well Known Text</i> (WKT) format.
+     * Formats this position in the <i>Well Known Text</i> (<abbr>WKT</abbr>) 
format.
      * The format is like below, where {@code x₀}, {@code x₁}, {@code x₂}, 
<i>etc.</i>
      * are the coordinate values at index 0, 1, 2, <i>etc.</i>:
      *
@@ -209,7 +213,7 @@ public abstract class AbstractDirectPosition extends 
FormattableObject implement
      * adjusted for the axis unit of measurement and the planet size if 
different than Earth).
      *
      * @param  formatter  the formatter where to format the inner content of 
this point.
-     * @return the WKT keyword, which is {@code "Point"} for this element.
+     * @return the <abbr>WKT</abbr> keyword, which is {@code "Point"} for this 
element.
      *
      * @since 1.0
      */
@@ -245,11 +249,11 @@ public abstract class AbstractDirectPosition extends 
FormattableObject implement
     /**
      * Implementation of the public {@link #toString()} and {@link 
DirectPosition2D#toString()} methods
      * for formatting a {@code POINT} element from a direct position in 
<i>Well Known Text</i>
-     * (WKT) format.
+     * (<abbr>WKT</abbr>) format.
      *
      * @param  position           the position to format.
      * @param  isSinglePrecision  {@code true} if every coordinate values can 
be cast to {@code float}.
-     * @return the point as a {@code POINT} in WKT format.
+     * @return the point as a {@code POINT} in <abbr>WKT</abbr> format.
      *
      * @see ArraysExt#isSinglePrecision(double[])
      */
@@ -277,9 +281,9 @@ public abstract class AbstractDirectPosition extends 
FormattableObject implement
     }
 
     /**
-     * Parses the given WKT.
+     * Parses the given <abbr>WKT</abbr>.
      *
-     * @param  wkt  the WKT to parse.
+     * @param  wkt  the <abbr>WKT</abbr> to parse.
      * @return the coordinates, or {@code null} if none.
      * @throws NumberFormatException if a number cannot be parsed.
      * @throws IllegalArgumentException if the parenthesis are not balanced.
@@ -394,7 +398,7 @@ parse:  while (i < length) {
 
     /**
      * Returns {@code true} if the specified object is also a {@code 
DirectPosition}
-     * with equal coordinates and equal CRS.
+     * with equal coordinates and equal <abbr>CRS</abbr>.
      *
      * This method performs the comparison as documented in the {@link 
DirectPosition#equals(Object)}
      * javadoc. In particular, the given object is not required to be of the 
same implementation class.
@@ -410,7 +414,7 @@ parse:  while (i < length) {
             return true;
         }
         if (object instanceof DirectPosition) {
-            final DirectPosition that = (DirectPosition) object;
+            final var that = (DirectPosition) object;
             final int dimension = getDimension();
             if (dimension == that.getDimension()) {
                 for (int i=0; i<dimension; i++) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralDirectPosition.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralDirectPosition.java
index 2fa37ecbac..4957278c50 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralDirectPosition.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralDirectPosition.java
@@ -30,10 +30,9 @@ import org.opengis.geometry.DirectPosition;
 import org.opengis.coordinate.MismatchedDimensionException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
 
-import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
-
 
 /**
  * A mutable {@code DirectPosition} (the coordinates of a position) of 
arbitrary dimension.
@@ -72,8 +71,8 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
     private static volatile Field coordinatesField;
 
     /**
-     * The coordinates of the direct position. The length of this array is the
-     * {@linkplain #getDimension() dimension} of this direct position.
+     * The coordinates of the direct position. The length of this array is
+     * the {@linkplain #getDimension() dimension} of this direct position.
      */
     public final double[] coordinates;
 
@@ -137,12 +136,12 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
     public GeneralDirectPosition(final DirectPosition point) {
         coordinates = point.getCoordinates();                            // 
Should already be cloned.
         crs = point.getCoordinateReferenceSystem();
-        ensureDimensionMatches("crs", coordinates.length, crs);
+        ArgumentChecks.ensureDimensionMatches("crs", coordinates.length, crs);
     }
 
     /**
-     * Constructs a position initialized to the values parsed
-     * from the given string in <i>Well Known Text</i> (WKT) format.
+     * Constructs a position initialized to the values parsed from the
+     * given string in <i>Well Known Text</i> (<abbr>WKT</abbr>) format.
      * The given string is typically a {@code POINT} element like below:
      *
      * {@snippet lang="wkt" :
@@ -176,9 +175,9 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
     }
 
     /**
-     * Returns the coordinate reference system in which the coordinate is 
given.
+     * Returns the coordinate reference system in which the coordinates are 
given.
      * May be {@code null} if this particular {@code DirectPosition} is 
included
-     * in a larger object with such a reference to a CRS.
+     * in a larger object with such a reference to a <abbr>CRS</abbr>.
      *
      * @return the coordinate reference system, or {@code null}.
      */
@@ -188,7 +187,7 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
     }
 
     /**
-     * Sets the coordinate reference system in which the coordinate is given.
+     * Sets the coordinate reference system in which the coordinates are given.
      *
      * @param  crs  the new coordinate reference system, or {@code null}.
      * @throws MismatchedDimensionException if the specified CRS does not have 
the expected number of dimensions.
@@ -196,12 +195,12 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
     public void setCoordinateReferenceSystem(final CoordinateReferenceSystem 
crs)
             throws MismatchedDimensionException
     {
-        ensureDimensionMatches("crs", getDimension(), crs);
+        ArgumentChecks.ensureDimensionMatches("crs", getDimension(), crs);
         this.crs = crs;
     }
 
     /**
-     * Returns a sequence of numbers that hold the coordinate of this position 
in its reference system.
+     * Returns a sequence of numbers that hold the coordinates of this 
position in its reference system.
      *
      * <div class="note"><b>API note:</b>
      * This method is final for ensuring consistency with the {@link 
#coordinates}, array field, which is public.</div>
@@ -216,7 +215,7 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
     }
 
     /**
-     * Sets the coordinate values along all dimensions.
+     * Sets the coordinate values in all dimensions.
      *
      * @param  coordinates  the new coordinates values, or a {@code null} 
array for
      *                      setting all coordinate values to {@link Double#NaN 
NaN}.
@@ -229,7 +228,7 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
         if (coordinates == null) {
             Arrays.fill(this.coordinates, Double.NaN);
         } else {
-            ensureDimensionMatches("coordinates", this.coordinates.length, 
coordinates);
+            ArgumentChecks.ensureDimensionMatches("coordinates", 
this.coordinates.length, coordinates);
             System.arraycopy(coordinates, 0, this.coordinates, 0, 
coordinates.length);
         }
     }
@@ -267,8 +266,8 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
 
     /**
      * Sets this coordinate to the specified direct position. If the specified 
position
-     * contains a coordinate reference system (CRS), then the CRS for this 
position will
-     * be set to the CRS of the specified position.
+     * contains a coordinate reference system (<abbr>CRS</abbr>), then the 
<abbr>CRS</abbr>
+     * for this position will be set to the <abbr>CRS</abbr> of the specified 
position.
      *
      * @param  position  the new position for this point,
      *                   or {@code null} for setting all coordinate values to 
{@link Double#NaN NaN}.
@@ -279,7 +278,7 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
         if (position == null) {
             Arrays.fill(coordinates, Double.NaN);
         } else {
-            ensureDimensionMatches("position", coordinates.length, position);
+            ArgumentChecks.ensureDimensionMatches("position", 
coordinates.length, position);
             
setCoordinateReferenceSystem(position.getCoordinateReferenceSystem());
             for (int i=0; i<coordinates.length; i++) {
                 coordinates[i] = position.getCoordinate(i);
@@ -317,7 +316,7 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
             if (field == null) {
                 coordinatesField = field = 
getCoordinatesField(GeneralDirectPosition.class);
             }
-            GeneralDirectPosition e = (GeneralDirectPosition) super.clone();
+            var e = (GeneralDirectPosition) super.clone();
             field.set(e, coordinates.clone());
             return e;
         } catch (ReflectiveOperationException | CloneNotSupportedException 
exception) {
@@ -335,7 +334,7 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
      */
     @Override
     public int hashCode() {
-        final int code = Arrays.hashCode(coordinates) + 
Objects.hashCode(getCoordinateReferenceSystem());
+        final int code = Arrays.hashCode(coordinates) + Objects.hashCode(crs);
         assert code == super.hashCode();
         return code;
     }
@@ -349,7 +348,7 @@ public class GeneralDirectPosition extends 
AbstractDirectPosition implements Ser
             return true;
         }
         if (object instanceof GeneralDirectPosition) {
-            final GeneralDirectPosition that = (GeneralDirectPosition) object;
+            final var that = (GeneralDirectPosition) object;
             return Arrays.equals(coordinates, that.coordinates) && 
Objects.equals(crs, that.crs);
         }
         return super.equals(object);                // Comparison of other 
implementation classes.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableDirectPosition.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableDirectPosition.java
new file mode 100644
index 0000000000..5df93bb69f
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableDirectPosition.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.geometry;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.io.Serializable;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.coordinate.MismatchedDimensionException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * An immutable {@code DirectPosition} (the coordinates of a position) of 
arbitrary dimension.
+ * This final class is immutable and thus inherently thread-safe if the {@link 
CoordinateReferenceSystem}
+ * instance given to the constructor is immutable. This is usually the case in 
Apache <abbr>SIS</abbr>.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.6
+ * @since   1.6
+ */
+public final class ImmutableDirectPosition extends AbstractDirectPosition 
implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -4275832076346637274L;
+
+    /**
+     * The coordinate reference system, or {@code null}.
+     */
+    @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
+    private final CoordinateReferenceSystem crs;
+
+    /**
+     * The coordinates of the direct position. The length of this array is
+     * the {@linkplain #getDimension() dimension} of this direct position.
+     */
+    private final double[] coordinates;
+
+    /**
+     * Constructs a position defined by a sequence of coordinate values.
+     *
+     * @param  crs          the <abbr>CRS</abbr> to assign to this direct 
position, or {@code null}.
+     * @param  coordinates  the coordinate values for each dimension.
+     * @throws MismatchedDimensionException if the CRS dimension is not equal 
to the number of coordinates.
+     */
+    public ImmutableDirectPosition(final CoordinateReferenceSystem crs, final 
double... coordinates)
+            throws MismatchedDimensionException
+    {
+        this.crs = crs;
+        this.coordinates = coordinates.clone();
+        ArgumentChecks.ensureDimensionMatches("crs", coordinates.length, crs);
+    }
+
+    /**
+     * Returns the given position as an {@code ImmutableDirectPosition} 
instance.
+     * If the given position is already an instance of {@code 
ImmutableDirectPosition},
+     * then it is returned unchanged. Otherwise, the coordinate values and the 
<abbr>CRS</abbr>
+     * of the given position are copied in a new position.
+     *
+     * @param  position  the position to cast or copy, or {@code null}.
+     * @return the values of the given position as an {@code 
ImmutableDirectPosition} instance.
+     */
+    public static ImmutableDirectPosition castOrCopy(final DirectPosition 
position) {
+        if (position == null || position instanceof ImmutableDirectPosition) {
+            return (ImmutableDirectPosition) position;
+        }
+        return new 
ImmutableDirectPosition(position.getCoordinateReferenceSystem(), 
position.getCoordinates());
+    }
+
+    /**
+     * The length of coordinate sequence (the number of entries).
+     *
+     * @return the dimensionality of this position.
+     */
+    @Override
+    public int getDimension() {
+        return coordinates.length;
+    }
+
+    /**
+     * Returns the coordinate reference system in which the coordinates are 
given.
+     * May be {@code null} if this particular {@code DirectPosition} is 
included
+     * in a larger object with such a reference to a <abbr>CRS</abbr>.
+     *
+     * @return the coordinate reference system, or {@code null}.
+     */
+    @Override
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return crs;
+    }
+
+    /**
+     * Returns a sequence of numbers that hold the coordinates of this 
position in its reference system.
+     *
+     * @return a copy of the coordinates array.
+     */
+    @Override
+    public double[] getCoordinates() {
+        return coordinates.clone();
+    }
+
+    /**
+     * Returns the coordinate at the specified dimension.
+     *
+     * @param  dimension  the dimension in the range 0 to {@linkplain 
#getDimension() dimension}-1.
+     * @return the coordinate at the specified dimension.
+     * @throws IndexOutOfBoundsException if the specified dimension is out of 
bounds.
+     */
+    @Override
+    public double getCoordinate(final int dimension) throws 
IndexOutOfBoundsException {
+        return coordinates[dimension];
+    }
+
+    /**
+     * @hidden because nothing new to said.
+     */
+    @Override
+    public String toString() {
+        return toString(this, ArraysExt.isSinglePrecision(coordinates));
+    }
+
+    /**
+     * @hidden because nothing new to said.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(coordinates) + Objects.hashCode(crs);
+    }
+
+    /**
+     * @hidden because nothing new to said.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object == this) {
+            return true;
+        }
+        if (object instanceof ImmutableDirectPosition) {
+            final var that = (ImmutableDirectPosition) object;
+            return Arrays.equals(coordinates, that.coordinates) && 
Objects.equals(crs, that.crs);
+        }
+        return super.equals(object);        // Comparison of other 
implementation classes.
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableEnvelope.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableEnvelope.java
index a47b29e745..33e8e65e75 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableEnvelope.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableEnvelope.java
@@ -29,18 +29,13 @@ import org.opengis.coordinate.MismatchedDimensionException;
 import org.opengis.coordinate.MismatchedCoordinateMetadataException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.metadata.extent.GeographicBoundingBox;
-
-import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
  * An immutable {@code Envelope} (a minimum bounding box or rectangle) of 
arbitrary dimension.
- * This class is final in order to ensure that the immutability contract 
cannot be broken
- * (assuming not using <i>Java Native Interface</i> or reflections).
- *
- * <h2>Immutability and thread safety</h2>
  * This final class is immutable and thus inherently thread-safe if the {@link 
CoordinateReferenceSystem}
- * instance given to the constructor is immutable. This is usually the case in 
Apache SIS.
+ * instance given to the constructor is immutable. This is usually the case in 
Apache <abbr>SIS</abbr>.
  *
  * @author  Cédric Briançon (Geomatys)
  * @author  Martin Desruisseaux (IRD, Geomatys)
@@ -82,7 +77,7 @@ public final class ImmutableEnvelope extends ArrayEnvelope 
implements Serializab
     {
         super(lowerCorner, upperCorner);
         this.crs = crs;
-        ensureDimensionMatches("crs", getDimension(), crs);
+        ArgumentChecks.ensureDimensionMatches("crs", getDimension(), crs);
     }
 
     /**
@@ -130,7 +125,7 @@ public final class ImmutableEnvelope extends ArrayEnvelope 
implements Serializab
     {
         super(envelope);
         this.crs = crs;
-        ensureDimensionMatches("crs", getDimension(), crs);
+        ArgumentChecks.ensureDimensionMatches("crs", getDimension(), crs);
     }
 
     /**
@@ -157,7 +152,7 @@ public final class ImmutableEnvelope extends ArrayEnvelope 
implements Serializab
     {
         super(wkt);
         this.crs = crs;
-        ensureDimensionMatches("crs", getDimension(), crs);
+        ArgumentChecks.ensureDimensionMatches("crs", getDimension(), crs);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index a55de2d6ad..16b670c870 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing;
 import java.util.Map;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.logging.Filter;
@@ -93,7 +94,6 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.OptionalCandidate;
 import org.apache.sis.util.Utilities;
-import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.internal.shared.Constants;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
@@ -165,6 +165,11 @@ public final class CRS {
      */
     static final int BIDIMENSIONAL = 2;
 
+    /**
+     * The {@value} value, for identifying code that assume three-dimensional 
objects.
+     */
+    static final int TRIDIMENSIONAL = 3;
+
     /**
      * Do not allow instantiation of this class.
      */
@@ -1229,22 +1234,125 @@ public final class CRS {
     }
 
     /**
-     * Gets or creates a coordinate reference system with a subset of the 
dimensions of the given CRS.
+     * Returns a mask of the dimensions where a {@code subCRS} element is 
found in the given <abbr>CRS</abbr>.
+     * If the given {@code crs} is {@linkplain #equivalent equivalent} to an 
element of the {@code subCRS} array,
+     * then this method returns a {@link BitSet} in which all bits are set to 
{@code true} in the range from 0 to
+     * <var>n</var>−1, where <var>n</var> is the number of dimensions of 
{@code crs}.
+     * Otherwise, if {@code crs} is an instance of {@link CompoundCRS}, then 
the {@code crs} components are traversed
+     * recursively and compared in the same way as described above, except 
that the range of bits set to {@code true}
+     * does not start at index 0. Instead, the range starts at the index of 
the first dimension of the matched
+     * {@code crs} component. If no match is found, then the returned {@link 
BitSet} is empty.
+     *
+     * <h4>Example</h4>
+     * The following snippet gets the dimensions of the temporal and vertical 
components of a <abbr>CRS</abbr>:
+     *
+     * {@snippet lang="java" :
+     *     CoordinateReferenceSystem crs = ...;
+     *     SingleCRS temporal = CRS.getTemporalComponent(crs);
+     *     SingleCRS vertical = CRS.getVerticalComponent(crs, false);
+     *     BitSet mask = CRS.locateDimensions(crs, vertical, temporal);
+     *     int[] dimensions = mask.stream().toArray();
+     *     }
+     *
+     * <p><b>Tip 1:</b> if only the index of the first dimension is desired, 
{@code mask.stream().toArray()}
+     * can be replaced by <code>mask.{@linkplain BitSet#nextSetBit(int) 
nextSetBit}(0)</code>.</p>
+     *
+     * <p><b>Tip 2:</b> for locating all dimensions <em>except</em> the 
vertical and temporal ones, use
+     * <code>mask.{@linkplain BitSet#flip(int, int) flip}(0, 
CRS.getDimensionOrZero(crs))</code>.</p>
+     *
+     * <h4>Null values</h4>
+     * This method is null-safe: if {@code crs} or {@code subCRS} is {@code 
null}, this method returns an empty set.
+     * If {@code subCRS} contains {@code null} elements, these elements are 
ignored. The latter makes easy to use
+     * directly the return value of methods such as {@link 
#getTemporalComponent(CoordinateReferenceSystem)}.
+     *
+     * @param  crs     the <abbr>CRS</abbr> for which the indexes of some 
dimensions are wanted.
+     * @param  subCRS  the <abbr>CRS</abbr>s to compare with {@code crs} or 
{@code crs} components.
+     * @return indexes of the dimensions where a {@code subCRS} element is 
found in {@code crs}.
+     *
+     * @see #selectDimensions(CoordinateReferenceSystem, BitSet, 
SeparationMode)
+     *
+     * @since 1.6
+     */
+    public static BitSet locateDimensions(final CoordinateReferenceSystem crs, 
final SingleCRS... subCRS) {
+        final var mask = new BitSet();
+        if (subCRS != null) {
+            int lower = 0;
+            for (final CoordinateReferenceSystem component : 
getSingleComponents(crs)) {
+                final int upper = lower + getDimensionOrZero(component);
+                for (final CoordinateReferenceSystem search : subCRS) {
+                    if (equivalent(component, search)) {
+                        mask.set(lower, upper);
+                    }
+                }
+                lower = upper;
+            }
+        }
+        return mask;
+    }
+
+    /**
+     * Gets or creates a coordinate reference system with a subset of the 
dimensions of the given <abbr>CRS</abbr>.
+     * The dimensions to retain are specified by a mask. The bit at index 0 
specifies whether to retain dimension 0,
+     * the bit at index 1 specifies whether to retain dimension 1, <i>etc</i>. 
After this method call,
+     * the given mask is updated to the dimensions that the {@code crs} 
dimensions that were effectively retained.
+     * The return value is always a <abbr>CRS</abbr> with axes in the same 
order as the given {@code crs}.
+     *
+     * <h4>Ellipsoidal height</h4>
+     * This method can transform a three-dimensional geographic 
<abbr>CRS</abbr> into a two-dimensional geographic
+     * <abbr>CRS</abbr>, i.e. this method can do the converse of {@link 
#compound(CoordinateReferenceSystem...)}.
+     * This method can also extract the {@linkplain 
CommonCRS.Vertical#ELLIPSOIDAL ellipsoidal height}
+     * from a three-dimensional geographic <abbr>CRS</abbr>, but this is 
generally not recommended because
+     * ellipsoidal heights make little sense without the (<var>latitude</var>, 
<var>longitude</var>) coordinates.
+     *
+     * <h4>Unseparable <abbr>CRS</abbr></h4>
+     * It is illegal to extract only the latitude or longitude axis from a 
geodetic <abbr>CRS</abbr>.
+     * Similar constraints exist also for projected and engineering 
<abbr>CRS</abbr>. If {@code crs}
+     * or a component of {@code crs} cannot be separated as requested by the 
{@code dimensions} mask,
+     * then the behavior of this method depends on the {@code mode} 
enumeration value:
+     *
+     * <ul>
+     *   <li>If {@link SeparationMode#EXACT}, then a {@link FactoryException} 
is thrown.</li>
+     *   <li>If {@link SeparationMode#OMIT_UNSEPARABLE}, then the result may 
contain less dimensions than requested.</li>
+     *   <li>If {@link SeparationMode#WHOLE_UNSEPARABLE}, then the result may 
contain more dimensions than requested.</li>
+     * </ul>
+     *
+     * @param  crs   the <abbr>CRS</abbr> to reduce the dimensionality, or 
{@code null} if none.
+     * @param  mask  on input, the dimensions to select. On output, the 
dimensions effectively selected.
+     * @param  mode  action to take if the {@code crs} cannot be separated in 
components at the requested dimensions.
+     * @return a coordinate reference system for the given dimensions, or 
{@code null} if the given {@code crs} was null.
+     * @throws IllegalArgumentException if the given {@code mask} is invalid.
+     * @throws FactoryException if this method needs to create a new 
<abbr>CRS</abbr> and that operation failed.
+     *
+     * @see #locateDimensions(CoordinateReferenceSystem, SingleCRS...)
+     * @see #compound(CoordinateReferenceSystem...)
+     *
+     * @since 1.6
+     */
+    public static CoordinateReferenceSystem selectDimensions(final 
CoordinateReferenceSystem crs, final BitSet mask, final SeparationMode mode)
+            throws FactoryException
+    {
+        ArgumentChecks.ensureNonNull("mask", mask);
+        ArgumentChecks.ensureNonNull("mode", mode);
+        return (crs == null) ? null : new Separator(mask, mode).reduce(crs);
+    }
+
+    /**
+     * Gets or creates a coordinate reference system with a subset of the 
dimensions of the given <abbr>CRS</abbr>.
      * This method can be used for dimensionality reduction, but not for 
changing axis order.
      * The specified dimensions are used as if they were in strictly 
increasing order without duplicated values.
      *
      * <h4>Ellipsoidal height</h4>
-     * This method can transform a three-dimensional geographic CRS into a 
two-dimensional geographic CRS.
-     * In this aspect, this method is the converse of {@link 
#compound(CoordinateReferenceSystem...)}.
+     * This method can transform a three-dimensional geographic 
<abbr>CRS</abbr> into a two-dimensional geographic
+     * <abbr>CRS</abbr>, i.e. this method can do the converse of {@link 
#compound(CoordinateReferenceSystem...)}.
      * This method can also extract the {@linkplain 
CommonCRS.Vertical#ELLIPSOIDAL ellipsoidal height}
-     * from a three-dimensional geographic CRS, but this is generally not 
recommended since ellipsoidal
-     * heights make little sense without their (<var>latitude</var>, 
<var>longitude</var>) locations.
+     * from a three-dimensional geographic <abbr>CRS</abbr>, but this is 
generally not recommended because
+     * ellipsoidal heights make little sense without the (<var>latitude</var>, 
<var>longitude</var>) coordinates.
      *
-     * @param  crs         the CRS to reduce the dimensionality, or {@code 
null} if none.
-     * @param  dimensions  the dimensions to retain. The dimensions will be 
taken in increasing order, ignoring duplicated values.
-     * @return a coordinate reference system for the given dimensions. May be 
the given {@code crs}, which may be {@code null}.
-     * @throws IllegalArgumentException if the given array is empty or if the 
array contains invalid indices.
-     * @throws FactoryException if this method needed to create a new CRS and 
that operation failed.
+     * @param  crs         the <abbr>CRS</abbr> to reduce the dimensionality, 
or {@code null} if none.
+     * @param  dimensions  the dimensions to retain. Will be taken in 
increasing order, ignoring duplicated values.
+     * @return a coordinate reference system for the given dimensions, or 
{@code null} if the given {@code crs} was null.
+     * @throws IllegalArgumentException if the content of the given {@code 
dimensions} array is invalid.
+     * @throws FactoryException if this method needs to create a new 
<abbr>CRS</abbr> and that operation failed.
      *
      * @see #getComponentAt(CoordinateReferenceSystem, int, int)
      * @see #compound(CoordinateReferenceSystem...)
@@ -1254,25 +1362,46 @@ public final class CRS {
     public static CoordinateReferenceSystem selectDimensions(final 
CoordinateReferenceSystem crs, final int... dimensions)
             throws FactoryException
     {
-        final var components = selectComponents(crs, dimensions);
-        return components.isEmpty() ? null : 
compound(components.toArray(CoordinateReferenceSystem[]::new));
+        ArgumentChecks.ensureNonNull("dimensions", dimensions);
+        return (crs == null) ? null : new Separator(dimensions).reduce(crs);
     }
 
     /**
-     * Gets or creates CRS components for a subset of the dimensions of the 
given <abbr>CRS</abbr>.
-     * The method performs the same work as {@link 
#selectDimensions(CoordinateReferenceSystem, int...)}
-     * except that it does not build new {@link CompoundCRS} instances when 
the specified dimensions span
-     * more than one {@linkplain DefaultCompoundCRS#getComponents() component}.
-     * Instead, the components are returned directly.
+     * Gets or creates <abbr>CRS</abbr> components for a subset of the 
dimensions of the given <abbr>CRS</abbr>.
+     * This method does the same work as <code>{@linkplain 
#selectDimensions(CoordinateReferenceSystem, BitSet,
+     * SeparationMode) selectDimensions}(crs, mask, mode)</code>, but without 
the final step creating a
+     * {@link CompoundCRS} from the selected components.
+     *
+     * @param  crs   the <abbr>CRS</abbr> from which to get a subset of the 
components, or {@code null} if none.
+     * @param  mask  on input, the dimensions to select. On output, the 
dimensions effectively selected.
+     * @param  mode  action to take if the {@code crs} cannot be separated in 
components at the requested dimensions.
+     * @return components in the specified dimensions, or an empty list if the 
specified {@code crs} is {@code null}.
+     * @throws IllegalArgumentException if the content of the given {@code 
dimensions} array is invalid.
+     * @throws FactoryException if this method needs to create a new 
<abbr>CRS</abbr> and that operation failed.
      *
-     * <p>While this method does not create new {@code CompoundCRS} instances, 
it may create other kinds
-     * of CRS for handling ellipsoidal height as documented in the {@code 
selectDimensions(…)} method.</p>
+     * @see #selectDimensions(CoordinateReferenceSystem, BitSet, 
SeparationMode)
+     *
+     * @since 1.6
+     */
+    public static List<CoordinateReferenceSystem> selectComponents(final 
CoordinateReferenceSystem crs,
+            final BitSet mask, final SeparationMode mode) throws 
FactoryException
+    {
+        ArgumentChecks.ensureNonNull("mask", mask);
+        ArgumentChecks.ensureNonNull("mode", mode);
+        return (crs == null) ? List.of() : new Separator(mask, 
mode).components(crs);
+    }
+
+    /**
+     * Gets or creates <abbr>CRS</abbr> components for a subset of the 
dimensions of the given <abbr>CRS</abbr>.
+     * This method does the same work as <code>{@linkplain 
#selectDimensions(CoordinateReferenceSystem, int...)
+     * selectDimensions}(crs, dimensions)</code>, but without the final step 
creating a {@link CompoundCRS} from
+     * the selected components.
      *
-     * @param  crs         the CRS from which to get a subset of the 
components, or {@code null} if none.
-     * @param  dimensions  the dimensions to retain. The dimensions will be 
taken in increasing order, ignoring duplicated values.
+     * @param  crs         the <abbr>CRS</abbr> from which to get a subset of 
the components, or {@code null} if none.
+     * @param  dimensions  the dimensions to retain. Will be taken in 
increasing order, ignoring duplicated values.
      * @return components in the specified dimensions, or an empty list if the 
specified {@code crs} is {@code null}.
-     * @throws IllegalArgumentException if the given array is empty or if the 
array contains invalid indices.
-     * @throws FactoryException if this method needed to create a new CRS and 
that operation failed.
+     * @throws IllegalArgumentException if the content of the given {@code 
dimensions} array is invalid.
+     * @throws FactoryException if this method needs to create a new 
<abbr>CRS</abbr> and that operation failed.
      *
      * @see #selectDimensions(CoordinateReferenceSystem, int...)
      *
@@ -1282,76 +1411,244 @@ public final class CRS {
             throws FactoryException
     {
         ArgumentChecks.ensureNonNull("dimensions", dimensions);
-        final int dimension = getDimensionOrZero(crs);
-        long selected = 0;
-        if (crs != null) {
-            for (final int d : dimensions) {
-                if (Objects.checkIndex(d, dimension) >= Long.SIZE) {
-                    throw new 
ArithmeticException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, 
d+1));
+        return (crs == null) ? List.of() : new 
Separator(dimensions).components(crs);
+    }
+
+    /**
+     * Action to take when a <abbr>CRS</abbr> cannot be separated in 
components at the requested dimensions.
+     * For example, a two-dimensional geographic <abbr>CRS</abbr> cannot be 
separated in a <abbr>CRS</abbr>
+     * containing only the latitude or only the longitude axis. If only the 
first dimension of such geographic
+     * <abbr>CRS</abbr> is requested, the action can be to throw an exception 
({@link #EXACT}),
+     * omit the unseparable geographic <abbr>CRS</abbr> from the separation 
result ({@link #OMIT_UNSEPARABLE}),
+     * or keep the whole geographic <abbr>CRS</abbr> with all its dimensions 
({@link #WHOLE_UNSEPARABLE}).
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.6
+     *
+     * @see #selectDimensions(CoordinateReferenceSystem, BitSet, 
SeparationMode)
+     *
+     * @since 1.6
+     */
+    public enum SeparationMode {
+        /**
+         * Separation result must contain exactly the requested dimensions.
+         * If the request cannot be satisfied, then a {@link FactoryException} 
will be throw.
+         */
+        EXACT,
+
+        /**
+         * Separation result contains only the components that can satisfy the 
requested dimensions.
+         * If a <abbr>CRS</abbr> cannot be separated in components at the 
requested dimensions, that
+         * <abbr>CRS</abbr> is excluded from the result. In other words, the 
result does not contain
+         * any dimension that was not requested, but some requested dimensions 
may be ignored.
+         */
+        OMIT_UNSEPARABLE,
+
+        /**
+         * Separation result contains components for all the requested 
dimensions. The result contains
+         * all requested dimensions, but may also contain some dimensions that 
were not requested.
+         */
+        WHOLE_UNSEPARABLE
+    }
+
+    /**
+     * Helper class for extracting some components of a <abbr>CRS</abbr>.
+     * The dimensions of the desired components are specified by a mask as a 
{@link BitSet}.
+     * It is possible to request the two-dimensional horizontal part of a 
three-dimensional
+     * geographic or projected <abbr>CRS</abbr>. It is also possible to 
request the ellipsoidal
+     * height of a 3D geographic or projected <abbr>CRS</abbr>, but this is 
not recommended.
+     */
+    private static final class Separator {
+        /** Mask of dimensions of the components to extract. */
+        private final BitSet mask;
+
+        /** Action when a <abbr>CRS</abbr> cannot be separated in components 
at the requested dimensions. */
+        private final SeparationMode mode;
+
+        /** The components selected from the specified dimensions. */
+        private final List<CoordinateReferenceSystem> components;
+
+        /** Next range of dimensions to select. */
+        private int lower, upper;
+
+        /**
+         * Creates a new separator for the specified dimensions.
+         *
+         * @param  dimensions  the dimensions to retain. Will be taken in 
increasing order, ignoring duplicated values.
+         * @throws IllegalArgumentException if {@code dimensions} array 
contains a negative index.
+         */
+        Separator(final int[] dimensions) {
+            this(new BitSet(), SeparationMode.EXACT);
+            for (int i : dimensions) {
+                try {
+                    mask.set(i);
+                } catch (IndexOutOfBoundsException e) {
+                    throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IndexOutOfBounds_1, i), e);
                 }
-                selected |= (1L << d);
             }
-            if (selected == 0) {
-                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, 
"dimensions"));
+        }
+
+        /**
+         * Creates a new separator for the specified dimensions specified as a 
mask.
+         *
+         * @param  mask  on input, the dimensions to select. On output, the 
dimensions effectively selected.
+         * @param  mode  action to take if the {@code crs} cannot be separated 
in components at the requested dimensions.
+         */
+        Separator(final BitSet mask, final SeparationMode mode) {
+            this.mask = mask;
+            this.mode = mode;
+            components = new ArrayList<>(mask.cardinality());
+        }
+
+        /**
+         * Gets or creates a coordinate reference system with a subset of the 
dimensions of the given <abbr>CRS</abbr>.
+         *
+         * @param  crs  the <abbr>CRS</abbr> to reduce the dimensionality.
+         * @return a coordinate reference system for the given dimensions.
+         * @throws FactoryException if this method needs to create a new 
<abbr>CRS</abbr> and that operation failed.
+         *
+         * @see #selectDimensions(CoordinateReferenceSystem, BitSet, 
SeparationMode)
+         */
+        CoordinateReferenceSystem reduce(final CoordinateReferenceSystem crs) 
throws FactoryException {
+            return 
compound(components(crs).toArray(CoordinateReferenceSystem[]::new));
+        }
+
+        /**
+         * Gets or creates <abbr>CRS</abbr> components for a subset of the 
dimensions of the given <abbr>CRS</abbr>.
+         *
+         * @param  crs  the <abbr>CRS</abbr> from which to get a subset of the 
components, or {@code null}.
+         * @return components in the specified dimensions, or an empty list if 
the specified {@code crs} is {@code null}.
+         * @throws FactoryException if this method needs to create a new 
<abbr>CRS</abbr> and that operation failed.
+         *
+         * @see #selectComponents(CoordinateReferenceSystem, BitSet, 
SeparationMode)
+         */
+        @SuppressWarnings("ReturnOfCollectionOrArrayField")
+        List<CoordinateReferenceSystem> components(final 
CoordinateReferenceSystem crs) throws FactoryException {
+            final int dimension = getDimensionOrZero(crs);
+            if (mode != SeparationMode.OMIT_UNSEPARABLE) {
+                final int i = mask.nextSetBit(dimension);
+                if (i >= 0) {
+                    throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IndexOutOfBounds_1, i));
+                }
             }
+            lower = mask.nextSetBit(0);
+            if (lower >= 0 && lower < dimension) {
+                upper = mask.nextClearBit(lower + 1);
+                reduce(crs, 0, dimension);
+            }
+            return components;
         }
-        final var components = new 
ArrayList<CoordinateReferenceSystem>(Long.bitCount(selected));
-        reduce(0, crs, dimension, selected, components);
-        return components;
-    }
 
-    /**
-     * Adds the components of reduced CRS into the given list.
-     * This method may invoke itself recursively for walking through compound 
CRS.
-     *
-     * @param  previous    number of dimensions of previous CRS.
-     * @param  crs         the CRS for which to select components.
-     * @param  dimension   number of dimensions of {@code crs}.
-     * @param  selected    bitmask of dimensions to select.
-     * @param  addTo       where to add CRS components.
-     * @return new bitmask after removal of dimensions of the components added 
to {@code addTo}.
-     */
-    private static long reduce(int previous, final CoordinateReferenceSystem 
crs, int dimension, long selected,
-                               final List<CoordinateReferenceSystem> addTo)
-            throws FactoryException
-    {
-        final long current = (Numerics.bitmask(dimension) - 1) << previous;
-        final long intersect = selected & current;
-choice: if (intersect != 0) {
-            if (intersect == current) {
-                addTo.add(crs);
-                selected &= ~current;
-            } else if (crs instanceof CompoundCRS) {
+        /**
+         * Adds selected components of the given {@code crs} into the {@link 
#components} list.
+         * This method may invoke itself recursively for walking through 
compound <abbr>CRS</abbr>.
+         *
+         * <p><b>Precondition:</b> caller must ensure that {@code lower} is 
between {@code offset}
+         * inclusive and {@code limit} exclusive. This is not verified by this 
method.</p>
+         *
+         * @param  crs     the <abbr>CRS</abbr> for which to select components.
+         * @param  offset  index of the first dimension of {@code crs} in the 
{@link #mask}.
+         * @param  limit   index after the last dimension of {@code crs} in 
the {@link #mask}.
+         * @return whether this method has added the given {@code crs} fully.
+         */
+        private boolean reduce(final CoordinateReferenceSystem crs, final int 
offset, final int limit)
+                throws FactoryException
+        {
+            assert lower >= offset && lower < limit : lower;
+            /*
+             * Unambiguous case where the next requested component is the 
whole `crs`.
+             * It may be a `CompoundCRS`, taken without separation of its 
components.
+             */
+            if (lower == offset && upper >= limit) {
+                return components.add(crs);
+            }
+            /*
+             * Decompose the `crs` in its components and repeat the operation 
for each of them.
+             * After each iteration, this block needs to ensure that `lower` 
and `upper` are up-to-date.
+             */
+            if (crs instanceof CompoundCRS) {
+                int end = offset;
+                boolean addedFully = true;
+                final int index = components.size();
                 for (final CoordinateReferenceSystem component : 
((CompoundCRS) crs).getComponents()) {
-                    dimension = getDimensionOrZero(component);
-                    selected = reduce(previous, component, dimension, 
selected, addTo);
-                    if ((selected & current) == 0) break;           // Stop if 
it would be useless to continue.
-                    previous += dimension;
+                    final int start = end;
+                    end += getDimensionOrZero(component);
+                    if (lower >= end) {
+                        addedFully = false;
+                    } else {
+                        addedFully &= reduce(component, start, end);
+                        lower = mask.nextSetBit(end);
+                        if (lower < 0) break;           // Stop if it would be 
useless to continue.
+                        if (lower >= upper) {
+                            upper = mask.nextClearBit(lower + 1);
+                        }
+                    }
                 }
-            } else if (dimension == 3) {
-                final GeodeticCRS baseCRS;
-                if (crs instanceof GeodeticCRS) {
-                    baseCRS = (GeodeticCRS) crs;
-                } else if (crs instanceof ProjectedCRS) {
-                    baseCRS = ((ProjectedCRS) crs).getBaseCRS();
-                } else {
-                    break choice;
+                /*
+                 * If all components were added, replace the components by the 
whole `crs` without
+                 * updating the mask, because the mask was already updated by 
the recursive calls.
+                 * Note that in `OMIT_UNSEPARABLE` mode, it is possible that 
no component was added.
+                 */
+                if (addedFully && end == limit) {
+                    components.subList(index, components.size()).clear();
+                    return components.add(crs);
                 }
-                final boolean isVertical = Long.bitCount(intersect) == 1;      
         // Presumed for now, verified later.
-                final int verticalDimension = 
Long.numberOfTrailingZeros((isVertical ? intersect : ~intersect) >>> previous);
-                final CoordinateSystemAxis verticalAxis = 
crs.getCoordinateSystem().getAxis(verticalDimension);
-                if (AxisDirections.isVertical(verticalAxis.getDirection())) 
try {
-                    addTo.add(new EllipsoidalHeightSeparator(baseCRS, 
isVertical).separate((SingleCRS) crs));
-                    selected &= ~current;
+                return false;
+            }
+            /*
+             * Special case for the decomposition of a three-dimensional 
geographic CRS into a horizontal
+             * or a vertical component. Extracting the vertical component is 
illegal according ISO 19111,
+             * but sometime useful as temporary information.
+             */
+            GeodeticCRS baseCRS = null;
+            if (crs instanceof GeodeticCRS) {
+                baseCRS = (GeodeticCRS) crs;
+            } else if (crs instanceof ProjectedCRS) {
+                baseCRS = ((ProjectedCRS) crs).getBaseCRS();
+            }
+            RuntimeException cause = null;
+            if (baseCRS != null) {
+                int i = lower;
+                int dimension = 0;              // Number of dimensions.
+                boolean isVertical = false;     // Whether the requested 
dimension is the vertical one.
+                do {
+                    dimension++;
+                    isVertical |= 
AxisDirections.isVertical(crs.getCoordinateSystem().getAxis(i).getDirection());
+                    i = mask.nextSetBit(i + 1);
+                } while (i >= 0 && i < limit);
+                if (dimension == (isVertical ? 1 : BIDIMENSIONAL)) try {
+                    components.add(new EllipsoidalHeightSeparator(baseCRS, 
isVertical).separate((SingleCRS) crs));
+                    return false;
                 } catch (IllegalArgumentException | ClassCastException e) {
-                    throw new 
FactoryException(Resources.format(Resources.Keys.CanNotSeparateCRS_1, 
crs.getName()));
+                    cause = e;
                 }
             }
+            /*
+             * Only some dimensions of the CRS are specified, but cannot 
separate `crs`.
+             * The failure to separate may be because the CRS is not geodetic, 
or because
+             * an exception occurred while trying to separate the geodetic CRS.
+             */
+            final boolean full;
+            switch (mode) {
+                case OMIT_UNSEPARABLE: {
+                    mask.clear(offset, limit);
+                    full = false;
+                    break;
+                }
+                case WHOLE_UNSEPARABLE: {
+                    mask.set(offset, limit);
+                    full = components.add(crs);
+                    break;
+                }
+                default: {
+                    throw new 
FactoryException(Resources.format(Resources.Keys.CanNotSeparateCRS_1, 
crs.getName()), cause);
+                }
+            }
+            if (cause != null) {
+                Logging.recoverableException(LOGGER, CRS.class, 
"selectComponents", cause);
+            }
+            return full;
         }
-        if ((selected & current) != 0) {
-            throw new 
FactoryException(Resources.format(Resources.Keys.CanNotSeparateCRS_1, 
crs.getName()));
-        }
-        return selected;
     }
 
     /**
@@ -1467,7 +1764,7 @@ choice: if (intersect != 0) {
             case BIDIMENSIONAL: {
                 return (SingleCRS) crs;
             }
-            case 3: {
+            case TRIDIMENSIONAL: {
                 /*
                  * The CRS would be horizontal if we can remove the vertical 
axis. CoordinateSystems.replaceAxes(…)
                  * will do this task for us. We can verify if the operation 
has been successful by checking that
@@ -1569,7 +1866,7 @@ choice: if (intersect != 0) {
                 }
             } while ((a = !a) == allowCreateEllipsoidal);
         }
-        if (allowCreateEllipsoidal && horizontalCode(crs) == 3) {
+        if (allowCreateEllipsoidal && horizontalCode(crs) == TRIDIMENSIONAL) {
             final CoordinateSystem cs = crs.getCoordinateSystem();
             final int i = AxisDirections.indexOfColinear(cs, AxisDirection.UP);
             if (i >= 0) {
@@ -1651,25 +1948,23 @@ choice: if (intersect != 0) {
      *
      * This method guaranteed that the returned list is a flat one as shown on 
the right side.
      * Note that such flat lists are the only one allowed by ISO/OGC standards 
for compound CRS.
-     * The hierarchical structure is an Apache SIS flexibility.
+     * The hierarchical structure is an Apache <abbr>SIS</abbr> flexibility.
      *
      * @param  crs  the coordinate reference system, or {@code null}.
      * @return the single coordinate reference systems, or an empty list if 
the given CRS is {@code null}.
-     * @throws ClassCastException if a CRS is neither a {@link SingleCRS} or a 
{@link CompoundCRS}.
+     * @throws ClassCastException if a <abbr>CRS</abbr> is neither a {@link 
SingleCRS} or a {@link CompoundCRS}.
      *
      * @see DefaultCompoundCRS#getSingleComponents()
      */
     public static List<SingleCRS> getSingleComponents(final 
CoordinateReferenceSystem crs) {
-        final List<SingleCRS> singles;
         if (crs == null) {
-            singles = List.of();
+            return List.of();
         } else if (crs instanceof CompoundCRS) {
-            singles = ((CompoundCRS) crs).getSingleComponents();
+            return ((CompoundCRS) crs).getSingleComponents();
         } else {
             // Intentional CassCastException here if the crs is not a 
SingleCRS.
-            singles = List.of((SingleCRS) crs);
+            return List.of((SingleCRS) crs);
         }
-        return singles;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/DirectPositionView.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/DirectPositionView.java
index 37101a406d..e34fa06be7 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/DirectPositionView.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/DirectPositionView.java
@@ -22,7 +22,8 @@ import org.apache.sis.geometry.AbstractDirectPosition;
 
 /**
  * A read-only direct position wrapping an array without performing any copy.
- * This class shall be used for temporary objects only (it is not serializable 
for this reason).
+ * This class shall be used for temporary objects only, unless the backing 
array is short.
+ * This class is not serializable for avoiding serialization of a potentially 
large array.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
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 153c5c23d0..676674b804 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
@@ -276,13 +276,11 @@ searchSrc:  while (sourceComponentIndex < 
sourceComponentIsUsed.length) {
                  */
                 int indexOfConstant = targetLowerDimension;     // Value for 
the default CRS.
                 final CoordinateReferenceSystem crs = 
coordinates.getCoordinateReferenceSystem();
-locate:         if (crs != null) {
-                    indexOfConstant = 0;
-                    for (SingleCRS component : CRS.getSingleComponents(crs)) {
-                        if (CRS.equivalent(targetComponent, component)) break 
locate;
-                        indexOfConstant += 
component.getCoordinateSystem().getDimension();
+                if (crs != null) {
+                    indexOfConstant = CRS.locateDimensions(crs, 
targetComponent).nextSetBit(0);
+                    if (indexOfConstant < 0) {
+                        return null;
                     }
-                    return null;
                 }
                 final int d = coordinates.getDimension();
                 final var c = new double[targetUpperDimension - 
targetLowerDimension];
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
index 7f2bdb6302..b919234ef2 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.List;
 import org.opengis.util.FactoryException;
 import org.opengis.util.NoSuchIdentifierException;
@@ -407,6 +408,16 @@ public final class CRSTest extends TestCaseWithLogs {
         loggings.assertNoUnexpectedLog();
     }
 
+    /**
+     * Tests getting a mask of some CRS components.
+     */
+    @Test
+    public void testLocateDimensions() {
+        // Following currently locate only the temporal CRS because geographic 
CRS is not separated in 2D+1D parts.
+        BitSet mask = CRS.locateDimensions(HardCodedCRS.WGS84_4D, 
HardCodedCRS.TIME, HardCodedCRS.ELLIPSOIDAL_HEIGHT);
+        assertEquals(BitSet.valueOf(new long[] {0b1000}), mask);
+    }
+
     /**
      * Tests {@link CRS#selectDimensions(CoordinateReferenceSystem, int[])} in 
the simpler case
      * where there is no three-dimensional geographic CRS to separate.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
index dd8495506a..8de693b5a4 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
@@ -39,6 +39,7 @@ import org.apache.sis.storage.base.StoreResource;
 import org.apache.sis.storage.base.GridResourceWrapper;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.internal.shared.DirectPositionView;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import static 
org.apache.sis.storage.geotiff.reader.GridGeometryBuilder.BIDIMENSIONAL;
 
@@ -216,22 +217,29 @@ final class MultiResolutionImage extends 
GridResourceWrapper implements StoreRes
         }
         double[] resolution = domain.getResolution(true);
         if (domain.isDefined(GridGeometry.CRS | GridGeometry.ENVELOPE)) try {
-            final CoordinateReferenceSystem crs = 
domain.getCoordinateReferenceSystem();
             CoordinateOperation op = lastOperation;
-            if (op == null || !crs.equals(op.getTargetCRS())) {
-                final GridGeometry gg = getGridGeometry();
-                op = CRS.findOperation(crs, gg.getCoordinateReferenceSystem(), 
gg.getGeographicExtent().orElse(null));
+            if (op == null || 
!domain.getCoordinateReferenceSystem().equals(op.getSourceCRS())) {
+                /*
+                 * The resolution in the user-supplied domain is associated to 
a CRS different than the CRS
+                 * of the last resolution that we computed. We must update the 
operation from user-supplied
+                 * resolution to the units of this grid coverage.
+                 */
+                final GridGeometry targetGrid = getGridGeometry();
+                final var context = new CoordinateOperationContext();
+                
targetGrid.getGeographicExtent().ifPresent(context::addAreaOfInterest);
+                
targetGrid.getConstantCoordinates().ifPresent(context::setConstantCoordinates);
+                op = CRS.findOperation(domain.getCoordinateMetadata(), 
targetGrid.getCoordinateMetadata(), context);
                 lastOperation = op;
             }
-            final MathTransform sourceToCoverage = op.getMathTransform();
-            if (!sourceToCoverage.isIdentity()) {
+            final MathTransform domainToCoverage = op.getMathTransform();
+            if (!domainToCoverage.isIdentity()) {
                 /*
                  * If the `domain` grid geometry has a resolution and an 
envelope, then it should have
                  * an extent and a "grid to CRS" transform (otherwise it may 
be a `GridGeometry` bug)
                  */
                 DirectPosition poi = new 
DirectPositionView.Double(domain.getExtent().getPointOfInterest(PixelInCell.CELL_CENTER));
                 poi = 
domain.getGridToCRS(PixelInCell.CELL_CENTER).transform(poi, null);
-                final MatrixSIS derivative = 
MatrixSIS.castOrCopy(sourceToCoverage.derivative(poi));
+                final MatrixSIS derivative = 
MatrixSIS.castOrCopy(domainToCoverage.derivative(poi));
                 resolution = derivative.multiply(resolution);
                 for (int i=0; i<resolution.length; i++) {
                     resolution[i] = Math.abs(resolution[i]);
diff --git a/netbeans-project/nbproject/project.xml 
b/netbeans-project/nbproject/project.xml
index bf34c520bf..4137422c50 100644
--- a/netbeans-project/nbproject/project.xml
+++ b/netbeans-project/nbproject/project.xml
@@ -39,6 +39,7 @@
             <word>programmatically</word>
             <word>transformative</word>
             <word>unary</word>
+            <word>unseparable</word>
             <word>untiled</word>
         </spellchecker-wordlist>
     </configuration>

Reply via email to