This is an automated email from the ASF dual-hosted git repository.

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

commit 0865c08dfb62c88d7d7b40a3a6c8144f6b981b7f
Merge: 7d5dfd074e 33688738c6
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sun Apr 16 19:42:02 2023 +0200

    Merge branch 'geoapi-3.1', omitting `CopyVisitor` internal class.

 .../apache/sis/gui/coverage/CoverageCanvas.java    |  59 +-
 .../apache/sis/gui/coverage/CoverageControls.java  |   5 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   | 120 ++-
 .../org/apache/sis/gui/map/ValuesFormatter.java    |   7 +-
 .../apache/sis/internal/gui/ImageConverter.java    |  13 +-
 .../apache/sis/internal/gui/control/ColorCell.java |  33 +-
 .../internal/gui/control/ColorColumnHandler.java   |  31 +-
 .../apache/sis/internal/gui/control/ColorRamp.java |  51 +-
 .../sis/internal/gui/control/ValueColorMapper.java |  12 -
 .../sis/internal/gui/control/package-info.java     |   2 +-
 .../sis/gui/coverage/CoverageStylingApp.java       |   5 +-
 .../apache/sis/cloud/aws/s3/CachedByteChannel.java |   2 +-
 .../org/apache/sis/coverage/BandedCoverage.java    |   9 +-
 .../java/org/apache/sis/coverage/Category.java     |   4 +-
 .../org/apache/sis/coverage/SampleDimension.java   | 109 ++-
 .../coverage/grid/BandAggregateGridCoverage.java   | 323 +++++++
 .../sis/coverage/grid/BufferedGridCoverage.java    |  22 +-
 .../sis/coverage/grid/ConvertedGridCoverage.java   |   2 +-
 .../coverage/grid/CoordinateOperationFinder.java   |   5 +-
 .../apache/sis/coverage/grid/DefaultEvaluator.java |  12 +-
 .../sis/coverage/grid/DerivedGridCoverage.java     |  27 +-
 .../apache/sis/coverage/grid/DimensionReducer.java |   5 +-
 .../sis/coverage/grid/DimensionalityReduction.java | 988 +++++++++++++++++++++
 .../sis/coverage/grid/DisjointExtentException.java |   9 +-
 .../coverage/grid/FractionalGridCoordinates.java   |   6 +-
 .../sis/coverage/grid/GridCoordinatesView.java     |   4 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java |  84 +-
 .../apache/sis/coverage/grid/GridCoverage2D.java   |   2 +
 .../sis/coverage/grid/GridCoverageBuilder.java     |  26 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   | 451 +++++++++-
 .../org/apache/sis/coverage/grid/GridExtent.java   | 261 +++---
 .../org/apache/sis/coverage/grid/GridGeometry.java |  10 +-
 .../apache/sis/coverage/grid/GridOrientation.java  |   2 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    | 103 ++-
 .../sis/coverage/grid/ReducedGridCoverage.java     | 160 ++++
 .../sis/coverage/grid/ResampledGridCoverage.java   |   3 +-
 .../apache/sis/coverage/grid/SliceGeometry.java    |  25 +-
 .../java/org/apache/sis/filter/LogicalFilter.java  |   5 +-
 .../java/org/apache/sis/image/AnnotatedImage.java  |  12 +-
 .../org/apache/sis/image/BandAggregateImage.java   | 338 +++++++
 .../java/org/apache/sis/image/BandSelectImage.java | 152 +++-
 .../org/apache/sis/image/BandSharedRaster.java     | 181 ++++
 .../java/org/apache/sis/image/BandSharing.java     | 377 ++++++++
 .../apache/sis/image/BandedSampleConverter.java    | 180 ++--
 .../main/java/org/apache/sis/image/Colorizer.java  | 348 ++++++++
 .../java/org/apache/sis/image/ComputedImage.java   |  88 +-
 .../java/org/apache/sis/image/ImageAdapter.java    |   6 +-
 .../java/org/apache/sis/image/ImageProcessor.java  | 408 ++++++++-
 .../java/org/apache/sis/image/Interpolation.java   |   6 +-
 .../main/java/org/apache/sis/image/MaskImage.java  |   2 +-
 .../org/apache/sis/image/MultiSourceImage.java     | 148 +++
 .../org/apache/sis/image/MultiSourceLayout.java    | 414 +++++++++
 .../org/apache/sis/image/MultiSourcePrefetch.java  | 178 ++++
 .../java/org/apache/sis/image/PlanarImage.java     |  95 +-
 .../java/org/apache/sis/image/RecoloredImage.java  |  47 +-
 .../java/org/apache/sis/image/ResampledImage.java  |  10 +-
 .../org/apache/sis/image/SourceAlignedImage.java   |   8 +-
 .../main/java/org/apache/sis/image/Transferer.java |  55 +-
 .../java/org/apache/sis/image/UserProperties.java  | 124 +++
 .../java/org/apache/sis/image/Visualization.java   | 244 +++--
 .../apache/sis/image/WritableComputedImage.java    | 177 ++++
 .../java/org/apache/sis/index/tree/PointTree.java  |   2 +-
 .../sis/internal/coverage/CommonDomainFinder.java  | 385 ++++++++
 .../sis/internal/coverage/MultiSourceArgument.java | 623 +++++++++++++
 .../sis/internal/coverage}/RangeArgument.java      |  46 +-
 .../sis/internal/coverage/SampleDimensions.java    |  56 +-
 .../j2d/{Colorizer.java => ColorModelBuilder.java} | 256 ++++--
 .../internal/coverage/j2d/ColorModelFactory.java   | 336 ++++---
 .../sis/internal/coverage/j2d/ColorModelType.java  |   2 +-
 .../sis/internal/coverage/j2d/ColorsForRange.java  | 166 +++-
 .../sis/internal/coverage/j2d/ImageLayout.java     |  34 +-
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  78 +-
 .../coverage/j2d/MultiBandsIndexColorModel.java    |  23 +-
 .../sis/internal/coverage/j2d/ObservableImage.java | 289 ++++++
 .../sis/internal/coverage/j2d/RasterFactory.java   |  34 +-
 .../internal/coverage/j2d/SampleModelFactory.java  |   2 +-
 .../internal/coverage/j2d/ScaledColorModel.java    |  15 +-
 .../internal/coverage/j2d/ScaledColorSpace.java    |  31 +-
 .../internal/coverage/j2d/WritableTiledImage.java  |   8 +-
 .../sis/internal/coverage/j2d/WriteSupport.java    | 100 ---
 .../apache/sis/internal/coverage/package-info.java |   2 +-
 .../org/apache/sis/internal/feature/Resources.java |  40 +
 .../sis/internal/feature/Resources.properties      |   8 +
 .../sis/internal/feature/Resources_fr.properties   |   8 +
 .../apache/sis/internal/filter/package-info.java   |   2 +-
 .../sis/internal/filter/sqlmm/SpatialFunction.java |  23 +-
 .../grid/BandAggregateGridCoverageTest.java        | 183 ++++
 .../coverage/grid/ConvertedGridCoverageTest.java   |  24 +-
 .../coverage/grid/DimensionalityReductionTest.java | 194 ++++
 .../apache/sis/coverage/grid/GridGeometryTest.java |  40 +-
 .../apache/sis/image/BandAggregateImageTest.java   | 536 +++++++++++
 .../org/apache/sis/image/BandSelectImageTest.java  |  79 +-
 .../org/apache/sis/image/ImageProcessorTest.java   |  62 +-
 .../apache/sis/image/StatisticsCalculatorTest.java |   2 +-
 .../java/org/apache/sis/image/TiledImageMock.java  |  66 +-
 .../sis/internal/coverage}/RangeArgumentTest.java  |  18 +-
 ...lorizerTest.java => ColorModelBuilderTest.java} |  20 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   8 +-
 .../apache/sis/internal/metadata/sql/Dialect.java  |  31 +-
 .../org/apache/sis/util/iso/DefaultScopedName.java |   2 +-
 .../java/org/apache/sis/test/sql/TestDatabase.java |  24 +-
 .../sis/internal/map/coverage/RenderingData.java   |  26 +-
 .../java/org/apache/sis/geometry/Envelopes.java    |   2 +-
 .../referencing/provider/DatumShiftGridLoader.java |   4 +-
 .../apache/sis/parameter/ParameterValueList.java   |   2 +-
 .../main/java/org/apache/sis/referencing/CRS.java  |  40 +-
 .../referencing/factory/sql/AuthorityCodes.java    |   2 +-
 .../operation/DefaultPassThroughOperation.java     | 141 +--
 .../operation/builder/LinearTransformBuilder.java  |   2 +-
 .../referencing/operation/projection/Mercator.java |   4 +-
 .../operation/transform/MathTransforms.java        |  37 +
 .../operation/transform/PassThroughTransform.java  | 127 ++-
 .../operation/transform/TransformSeparator.java    |  55 +-
 .../transform/PassThroughTransformTest.java        |  65 +-
 .../apache/sis/internal/system/Configuration.java  |   2 +-
 .../org/apache/sis/internal/util/Numerics.java     |  13 +
 .../java/org/apache/sis/measure/NumberRange.java   |   7 +-
 .../main/java/org/apache/sis/measure/Range.java    |  23 +-
 .../java/org/apache/sis/measure/SystemUnit.java    |   2 +-
 .../java/org/apache/sis/util/ArgumentChecks.java   | 112 ++-
 .../main/java/org/apache/sis/util/ArraysExt.java   | 140 +--
 .../src/main/java/org/apache/sis/util/Version.java |   2 +-
 .../sis/util/collection/WeakValueHashMap.java      |  89 +-
 .../java/org/apache/sis/util/package-info.java     |   2 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 .../java/org/apache/sis/measure/RangeTest.java     |  17 +-
 .../org/apache/sis/measure/UnitFormatTest.java     |   2 +-
 .../java/org/apache/sis/measure/UnitsTest.java     |   2 +-
 .../org/apache/sis/util/ArgumentChecksTest.java    |   6 +-
 .../java/org/apache/sis/util/ArraysExtTest.java    |  32 +-
 .../apache/sis/storage/geotiff/GeoCodesTest.java   |   1 -
 .../org/apache/sis/internal/netcdf/Convention.java |   3 +-
 .../org/apache/sis/internal/netcdf/Raster.java     |  11 +-
 .../apache/sis/internal/netcdf/RasterResource.java |   6 +-
 .../apache/sis/internal/sql/feature/Database.java  |  31 +-
 .../sis/internal/sql/feature/ValueGetter.java      | 139 ++-
 .../sis/internal/sql/feature/package-info.java     |   2 +-
 .../apache/sis/internal/sql/postgis/Postgres.java  |   9 +-
 .../sis/internal/sql/postgis/RasterReader.java     |   3 +-
 .../sis/internal/sql/postgis/package-info.java     |   2 +-
 .../sql/feature/TemporalValueGetterTest.java       | 238 +++++
 .../org/apache/sis/storage/sql/SQLStoreTest.java   | 104 +--
 .../apache/sis/storage/sql/TestOnAllDatabases.java |  99 +++
 .../org/apache/sis/test/suite/SQLTestSuite.java    |   1 +
 .../sis/internal/storage/GridResourceWrapper.java  |   4 +
 .../sis/internal/storage/MemoryFeatureSet.java     |  12 +-
 .../sis/internal/storage/MemoryGridResource.java   |  44 +-
 .../org/apache/sis/internal/storage/Resources.java |  14 +-
 .../sis/internal/storage/Resources.properties      |   2 -
 .../sis/internal/storage/Resources_fr.properties   |   2 -
 .../sis/internal/storage/TiledGridCoverage.java    |   2 +-
 .../sis/internal/storage/TiledGridResource.java    |   4 +-
 .../sis/internal/storage/esri/AsciiGridStore.java  |   2 +-
 .../sis/internal/storage/esri/RasterStore.java     |  18 +-
 .../sis/internal/storage/esri/RawRasterReader.java |   6 +-
 .../sis/internal/storage/esri/RawRasterStore.java  |   2 +-
 .../apache/sis/internal/storage/folder/Store.java  |   2 +-
 .../internal/storage/image/WorldFileResource.java  |   2 +-
 .../sis/internal/storage/io/IOUtilities.java       |   6 +-
 .../org/apache/sis/storage/AbstractFeatureSet.java |  14 +-
 .../sis/storage/AbstractGridCoverageResource.java  |  14 +-
 .../org/apache/sis/storage/AbstractResource.java   |  20 +-
 .../java/org/apache/sis/storage/CoverageQuery.java | 123 ++-
 .../org/apache/sis/storage/CoverageSubset.java     | 186 ++--
 .../java/org/apache/sis/storage/FeatureSubset.java |   4 +-
 .../apache/sis/storage/GridCoverageResource.java   |   6 +-
 .../org/apache/sis/storage/StorageConnector.java   |  44 +-
 .../storage/aggregate/AggregatedFeatureSet.java    |  13 +-
 .../sis/storage/aggregate/AggregatedResource.java  |  22 +-
 .../aggregate/BandAggregateGridResource.java       | 450 ++++++++++
 .../storage/aggregate/ConcatenatedFeatureSet.java  |   8 +-
 .../aggregate/ConcatenatedGridCoverage.java        |   2 +-
 .../aggregate/ConcatenatedGridResource.java        |  99 ++-
 .../sis/storage/aggregate/CoverageAggregator.java  | 252 +++++-
 .../apache/sis/storage/aggregate/GridSlice.java    |  79 +-
 .../sis/storage/aggregate/GridSliceLocator.java    |   3 +-
 .../org/apache/sis/storage/aggregate/Group.java    |   4 +-
 .../sis/storage/aggregate/GroupAggregate.java      |  33 +-
 .../apache/sis/storage/aggregate/GroupByCRS.java   |   4 +-
 .../sis/storage/aggregate/GroupBySample.java       |   8 +-
 .../sis/storage/aggregate/GroupByTransform.java    |   8 +-
 .../sis/storage/aggregate/JoinFeatureSet.java      |  10 +-
 .../sis/storage/aggregate/MergeStrategy.java       |   7 +-
 .../internal/storage/MemoryGridResourceTest.java   |   2 -
 .../org/apache/sis/storage/CoverageSubsetTest.java | 130 +++
 .../aggregate/BandAggregateGridResourceTest.java   | 228 +++++
 .../storage/aggregate/CoverageAggregatorTest.java  |   6 +-
 ...AggregatorTest.java => OpaqueGridResource.java} |  35 +-
 .../apache/sis/test/suite/StorageTestSuite.java    |   3 +-
 191 files changed, 11834 insertions(+), 2172 deletions(-)

diff --cc 
core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
index bb68262ecf,f0a24e8c9e..f563a8e813
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@@ -62,10 -62,9 +62,9 @@@ import org.apache.sis.util.Debug
   *   <tr><td>[10…210]</td>     <td>Temperature to be converted into Celsius 
degrees through a linear equation</td></tr>
   * </table>
   * In this example, sample values in range [10…210] define a quantitative 
category, while all others categories are qualitative.
-  * </div>
   *
   * <h2>Relationship with metadata</h2>
 - * This class provides the same information than ISO 19115 {@link 
org.opengis.metadata.content.SampleDimension},
 + * This class provides the same information than ISO 19115 {@code 
org.opengis.metadata.content.SampleDimension},
   * but organized in a different way. The use of the same name may seem a 
risk, but those two types are typically
   * not used at the same time.
   *
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
index 0000000000,517edb4390..09b7dcbf0d
mode 000000,100644..100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
@@@ -1,0 -1,989 +1,988 @@@
+ /*
+  * 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.coverage.grid;
+ 
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.util.Arrays;
+ import java.util.BitSet;
+ import java.util.function.UnaryOperator;
+ import java.io.Serializable;
+ import org.opengis.util.FactoryException;
+ import org.opengis.geometry.DirectPosition;
+ import org.opengis.geometry.MismatchedDimensionException;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.opengis.referencing.operation.MathTransformFactory;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.datum.PixelInCell;
+ import org.apache.sis.util.Utilities;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.ComparisonMode;
+ import org.apache.sis.internal.util.Numerics;
+ import org.apache.sis.internal.util.ArgumentCheckByAssertion;
+ import org.apache.sis.internal.feature.Resources;
+ import org.apache.sis.geometry.ImmutableEnvelope;
+ import org.apache.sis.coverage.SubspaceNotSpecifiedException;
+ import org.apache.sis.geometry.GeneralDirectPosition;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.referencing.operation.transform.TransformSeparator;
+ import org.apache.sis.referencing.operation.transform.PassThroughTransform;
+ import org.apache.sis.referencing.CRS;
+ 
+ // Branch-dependent imports
 -import org.opengis.coverage.PointOutsideCoverageException;
++import org.apache.sis.coverage.PointOutsideCoverageException;
+ 
+ 
+ /**
+  * Description about how to reduce the number of dimensions of the domain of 
a grid coverage.
+  * This is a reduction in the number of dimensions of the grid extent, which 
usually implies
+  * a reduction in the number of dimensions of the CRS but not necessarily at 
the same indices
+  * (the relationship between grid dimensions and CRS dimensions is not 
necessarily straightforward).
+  * The sample dimensions (coverage range) are unmodified.
+  *
+  * <p>{@code DimensionalityReduction} specifies which dimensions to keep, and 
which grid
+  * values to use for the omitted dimensions. This information allows the 
conversion from
+  * a source {@link GridGeometry} to a reduced grid geometry, and 
conversely.</p>
+  *
+  * <p>Instances of {@code DimensionalityReduction} are immutable and 
thread-safe.</p>
+  *
+  * <h2>Assumptions</h2>
+  * The current implementation assumes that removing <var>n</var> dimensions 
in the grid extent
+  * causes the removal of exactly <var>n</var> dimensions in the Coordinate 
Reference System.
+  * However, the removed dimensions do not need to be at the same indices or 
in same order.
+  *
+  * @author  Alexis Manin (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.4
+  * @since   1.4
+  */
+ public class DimensionalityReduction implements UnaryOperator<GridCoverage>, 
Serializable {
+     /**
+      * For cross-version compatibility.
+      */
+     private static final long serialVersionUID = -6462887684250336261L;
+ 
+     /**
+      * The source grid geometry with all dimensions.
+      *
+      * @see #getSourceGridGeometry()
+      */
+     private final GridGeometry sourceGeometry;
+ 
+     /**
+      * The reduced grid geometry.
+      * The number of dimensions shall be the number of bits set in the {@link 
#gridAxesToPass} bitmask.
+      *
+      * @see #getReducedGridGeometry()
+      */
+     private final GridGeometry reducedGeometry;
+ 
+     /**
+      * Indices of source grid dimensions to keep in the reduced grid.
+      * This is the parameter of the "pass-through" coordinate operation.
+      * Values must be in strictly increasing order.
+      *
+      * @see #getSelectedDimensions()
+      */
+     private final int[] gridAxesToPass;
+ 
+     /**
+      * Indices of target dimensions that have been removed.
+      * Values must be in strictly increasing order.
+      */
+     private final int[] crsAxesToRemove;
+ 
+     /**
+      * Partially filled array of CRS components to use for building a 
compound CRS.
+      * Elements in this array are either instances of {@link 
CoordinateReferenceSystem} or {@link Integer}.
+      * The CRS elements are components in the dimensions that were removed, 
while the integer elements are
+      * slots where to insert components of a reduced CRS with the number of 
dimensions given by the integer.
+      * This array is {@code null} if at least one CRS component cannot be 
isolated.
+      */
+     @SuppressWarnings("serial")                                 // Most SIS 
implementations are serializable.
+     private final Object[] componentsOfCRS;
+ 
+     /**
+      * The part of the "grid to CRS" transform which has been removed in the 
reduced grid geometry.
+      * The number of source and target dimensions are the same than in the 
source grid geometry.
+      * The dimensions identified by {@link #gridAxesToPass} are pass-through 
dimensions.
+      *
+      * @see #getRemovedGridToCRS(PixelInCell)
+      */
+     @SuppressWarnings("serial")                                 // Most SIS 
implementations are serializable.
+     private final MathTransform removedGridToCRS, removedCornerToCRS;
+ 
+     /**
+      * Grid coordinates to use in {@code reverse(…)} method calls for 
reconstituting some removed dimensions.
+      * Keys are grid dimensions of the source that are <em>not</em> in the 
{@link #gridAxesToPass} array.
+      * Values are grid coordinates to assign to the source grid extent at the 
dimension identified by the key.
+      * This map does not need to contain an entry for all removed dimensions.
+      *
+      * @see #getSliceCoordinates()
+      */
+     @SuppressWarnings("serial")                                 // Map.of(…) 
are serializable.
+     private final Map<Integer,Long> sliceCoordinates;
+ 
+     /**
+      * A cache of {@link #gridAxesToPass} for all combinations of axes to 
retain in the first four dimensions.
+      * We use this cache because the same sequences of dimension indices will 
be created most of the times.
+      */
+     private static final int[][] CACHED = new int[1 << 4][];    // Length 
must be a power of 2.
+ 
+     /**
+      * Returns the indices for which {@code axes} contains a bit in the set 
state.
+      * This method may return a cached instance, <strong>do not 
modify.</strong>
+      * Elements in the returned array are in strictly increasing order.
+      */
+     private static int[] toArray(final BitSet axes) {
+         if (axes.length() >= CACHED.length) {
+             return axes.stream().toArray();
+         }
+         final int bitmask = (int) axes.toLongArray()[0];
+         int[] indices;
+         synchronized (CACHED) {
+             indices = CACHED[bitmask];
+             if (indices == null) {
+                 CACHED[bitmask] = indices = axes.stream().toArray();
+             }
+         }
+         return indices;
+     }
+ 
+     /**
+      * Reduces the dimension of the specified grid geometry by retaining the 
axes specified in the given bitset.
+      * Axes in the reduced grid geometry will be in the same order than in 
the source geometry:
+      *
+      * @param  source    the grid geometry on which to select a subset of its 
grid dimensions.
+      * @param  gridAxes  bitmask of indices of source grid dimensions to keep 
in the reduced grid.
+      *                   Will be modified by this constructor for internal 
purpose.
+      * @param  factory   the factory to use for creating new math transforms, 
or {@code null} if none.
+      * @throws FactoryException if the dimensions to kept cannot be separated 
from the dimensions to omit.
+      */
+     protected DimensionalityReduction(final GridGeometry source, final BitSet 
gridAxes, final MathTransformFactory factory)
+             throws FactoryException
+     {
+         gridAxesToPass   = toArray(gridAxes);
+         sliceCoordinates = Map.of();
+         sourceGeometry   = source;
+         /*
+          * Set `gridAxes` to its complement: instead of dimensions to pass, 
it will become
+          * the dimensions to remove. If the result is empty, we have an 
identity operation.
+          */
+         final int sourceDim = source.getDimension();
+         gridAxes.flip(0, sourceDim);
+         if (gridAxes.isEmpty()) {
+             reducedGeometry = source;
+             crsAxesToRemove = ArraysExt.EMPTY_INT;
+             componentsOfCRS = null;
+         } else {
+             /*
+              * The calculation of `dimSubCRS` below assumes that 1 removed 
grid dimension
+              * implies 1 removed CRS dimension. See "assumptions" in class 
javadoc.
+              */
+             final int targetDim = source.getTargetDimension();
+             final int dimSubCRS = targetDim - (sourceDim - 
gridAxesToPass.length);
+             final var helper    = new SliceGeometry(source, null, 
gridAxesToPass, factory);
+             reducedGeometry = helper.reduce(null, dimSubCRS);
+             /*
+              * Get the sequence of CRS axes to remove. The result will often 
be
+              * the same indices than `gridAxesToRemove`, but not necessarily.
+              */
+             final BitSet crsAxes = bitmask(helper.getTargetDimensions(), 
targetDim);
+             crsAxes.flip(0, targetDim);
+             crsAxesToRemove = toArray(crsAxes);
+             if (source.isDefined(GridGeometry.CRS)) {
+                 componentsOfCRS = 
filterCRS(source.getCoordinateReferenceSystem(), crsAxes);
+             } else {
+                 componentsOfCRS = null;
+             }
+             if (source.isDefined(GridGeometry.GRID_TO_CRS)) {
+                 final int[] gridAxesToRemove = gridAxes.stream().toArray();
+                 removedGridToCRS   = filterGridToCRS(gridAxesToRemove, 
gridAxes, PixelInCell.CELL_CENTER, factory);
+                 removedCornerToCRS = filterGridToCRS(gridAxesToRemove, 
gridAxes, PixelInCell.CELL_CORNER, factory);
+                 return;
+             }
+         }
+         removedGridToCRS   = null;
+         removedCornerToCRS = null;
+     }
+ 
+     /**
+      * Returns all CRS components for the dimensions where the bit is set.
+      * There is one CRS for each range of consecutive dimension indices.
+      * If at least one CRS cannot be fetched, then this method returns {@code 
null}.
+      *
+      * @param  crs   the CRS for which to get components.
+      * @param  axes  dimensions (or axis indices) of the components to get.
+      * @throws FactoryException if the geodetic factory failed to create a 
compound CRS.
+      * @return CRS for each range of consecutive axis indices.
+      */
+     private static Object[] filterCRS(final CoordinateReferenceSystem crs, 
final BitSet axes)
+             throws FactoryException
+     {
+         final int dim = crs.getCoordinateSystem().getDimension();
+         final var components = new Object[dim];
+         int count = 0;
+         int upper = 0;
+         int lower;
+         while ((lower = axes.nextSetBit(upper)) >= 0) {
+             if (lower != upper) {
+                 components[count++] = lower - upper;        // Here `upper` 
is not yet updated to the higher value.
+             }
+             upper = axes.nextClearBit(lower);
+             for (CoordinateReferenceSystem c : CRS.selectComponents(crs, 
ArraysExt.range(lower, upper))) {
+                 components[count++] = c;
+             }
+         }
+         if (upper != dim) {
+             components[count++] = dim - upper;              // Keep an empty 
slot for the reduced CRS component.
+         }
+         return ArraysExt.resize(components, count);
+     }
+ 
+     /**
+      * Returns a "grid to CRS" transform which will transform only the 
"removed" dimensions.
+      * Other dimensions are passed-through.
+      *
+      * @param  gridAxesToRemove  the dimensions on which to operate.
+      * @param  bitset   same as {@link gridAxesToRemove} but as a bit set 
(for efficiency).
+      * @param  anchor   whether to compute the transform for pixel corner or 
pixel center.
+      * @param  factory  the factory to use for creating new math transforms, 
or {@code null} if none.
+      */
+     private MathTransform filterGridToCRS(final int[] gridAxesToRemove, final 
BitSet bitset, final PixelInCell anchor,
+             final MathTransformFactory factory) throws FactoryException
+     {
+         final MathTransform gridToCRS = sourceGeometry.getGridToCRS(anchor);
+         final var sep = new TransformSeparator(gridToCRS, factory);
+         sep.addSourceDimensions(gridAxesToRemove);
+         sep.addTargetDimensions(crsAxesToRemove);
+         return PassThroughTransform.create(bitset, sep.separate(), 
gridToCRS.getSourceDimensions(), factory);
+     }
+ 
+     /**
+      * Creates the same dimensionality reduction as the specified {@code 
source}, but with different slice indices.
+      *
+      * @param  source  the dimensionality reduction to copy.
+      * @param  slice   coordinates of the slice in removed dimensions.
+      */
+     private DimensionalityReduction(final DimensionalityReduction source, 
final Map<Integer,Long> slice) {
+         sourceGeometry     = source.sourceGeometry;
+         reducedGeometry    = source.reducedGeometry;
+         gridAxesToPass     = source.gridAxesToPass;
+         crsAxesToRemove    = source.crsAxesToRemove;
+         componentsOfCRS    = source.componentsOfCRS;
+         removedGridToCRS   = source.removedGridToCRS;
+         removedCornerToCRS = source.removedCornerToCRS;
+         sliceCoordinates   = Map.copyOf(slice);
+     }
+ 
+     /**
+      * Returns a new bitmask of all dimension indices in the axes array.
+      * The returned object can be safely modified.
+      *
+      * @param  axes       indices of axes to pass or to remove.
+      * @param  sourceDim  maximal valid dimension index + 1.
+      * @return bitmask of dimensions in the given array.
+      * @throws IndexOutOfBoundsException if an axis index is out of bounds.
+      */
+     private static BitSet bitmask(final int[] axes, final int sourceDim) {
+         final BitSet bitmask = new BitSet(sourceDim);
+         for (final int dim : axes) {
+             ArgumentChecks.ensureValidIndex(sourceDim, dim);
+             bitmask.set(dim);
+         }
+         return bitmask;
+     }
+ 
+     /**
+      * Reduces the dimension of the specified grid geometry by retaining only 
the specified axes.
+      * Axes in the reduced grid geometry will be in the same order than in 
the source geometry:
+      * change of axis order and duplicated values in the {@code 
gridAxesToPass} argument are ignored.
+      *
+      * @param  source          the grid geometry to reduce.
+      * @param  gridAxesToPass  the grid axes to retain, ignoring order and 
duplicated values.
+      * @return reduced grid geometry together with other information.
+      * @throws IndexOutOfBoundsException if a grid axis index is out of 
bounds.
+      * @throws IllegalGridGeometryException if the dimensions to kept cannot 
be separated from the dimensions to omit.
+      */
+     public static DimensionalityReduction select(final GridGeometry source, 
final int... gridAxesToPass) {
+         ArgumentChecks.ensureNonNull("source", source);
+         final BitSet bitmask = bitmask(gridAxesToPass, source.getDimension());
+         try {
+             return new DimensionalityReduction(source, bitmask, null);
+         } catch (FactoryException e) {
+             throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions,
 e));
+         }
+     }
+ 
+     /**
+      * A convenience method for selecting the two first dimensions of the 
specified grid geometry.
+      * This method can be used as a lambda function in resources query. 
Example:
+      *
+      * {@snippet lang="java" :
+      *     CoverageQuery query = new CoverageQuery();
+      *     query.setAxisSelection(DimensionalityReduction::select2D);
+      *     }
+      *
+      * @param  source  the grid geometry to reduce.
+      * @return reduced grid geometry together with other information.
+      * @throws IndexOutOfBoundsException if the grid geometry does not have 
at least two dimensions.
+      * @throws IllegalGridGeometryException if the dimensions to kept cannot 
be separated from the dimensions to omit.
+      *
+      * @see org.apache.sis.storage.CoverageQuery#setAxisSelection(Function)
+      */
+     public static DimensionalityReduction select2D(final GridGeometry source) 
{
+         return select(source, 0, 1);
+     }
+ 
+     /**
+      * Reduces the dimension of the specified grid geometry by removing the 
specified axes.
+      * Axes in the reduced grid geometry will be in the same order than in 
the source geometry:
+      * axis order and duplicated values in the {@code gridAxesToRemove} 
argument are not significant.
+      *
+      * @param  source            the grid geometry to reduce.
+      * @param  gridAxesToRemove  the grid axes to remove, ignoring order and 
duplicated values.
+      * @return reduced grid geometry together with other information.
+      * @throws IndexOutOfBoundsException if a grid axis index is out of 
bounds.
+      * @throws IllegalGridGeometryException if the dimensions to kept cannot 
be separated from the dimensions to omit.
+      */
+     public static DimensionalityReduction remove(final GridGeometry source, 
final int... gridAxesToRemove) {
+         ArgumentChecks.ensureNonNull("source", source);
+         final int sourceDim = source.getDimension();
+         final BitSet bitmask = bitmask(gridAxesToRemove, sourceDim);
+         bitmask.flip(0, sourceDim);
+         try {
+             return new DimensionalityReduction(source, bitmask, null);
+         } catch (FactoryException e) {
+             throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions,
 e));
+         }
+     }
+ 
+     /**
+      * Automatically reduces a grid geometry by removing all grid dimensions 
with an extent size of 1.
+      * Axes in the reduced grid geometry will be in the same order than in 
the source geometry.
+      *
+      * @param  source  the grid geometry to reduce.
+      * @return reduced grid geometry together with other information.
+      * @throws IncompleteGridGeometryException if the grid geometry has no 
extent.
+      * @throws IllegalGridGeometryException if the dimensions to kept cannot 
be separated from the dimensions to omit.
+      *
+      * @see #select2D(GridGeometry)
+      */
+     public static DimensionalityReduction reduce(final GridGeometry source) {
+         ArgumentChecks.ensureNonNull("source", source);
+         final GridExtent extent = source.getExtent();
+         final int sourceDim = extent.getDimension();
+         final BitSet bitmask = new BitSet(sourceDim);
+         for (int dim=0; dim < sourceDim; dim++) {
+             if (extent.getLow(dim) != extent.getHigh(dim)) {
+                 bitmask.set(dim);
+             }
+         }
+         try {
+             return new DimensionalityReduction(source, bitmask, null);
+         } catch (FactoryException e) {
+             throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions,
 e));
+         }
+     }
+ 
+     /**
+      * Returns {@code true} if this object does not reduce any dimension.
+      * It may happen if {@code select(…)} has been invoked with all axes to 
keep,
+      * or if {@code remove(…)} has been invoked with no axis to remove.
+      *
+      * @return whether this {@code DimensionalityReduction} does nothing.
+      */
+     public boolean isIdentity() {
+         return reducedGeometry == sourceGeometry;
+     }
+ 
+     /**
+      * Returns {@code true} if this dimensionality reduction is a slice in 
the source coverage.
+      * This is true if all removed dimensions either have a {@linkplain 
GridExtent#getSize(int)
+      * grid size} of one cell, or have a {@linkplain #getSliceCoordinates() 
slice coordinate} specified.
+      *
+      * <p>If this method returns {@code false}, then the results of {@code 
reverse(…)} method calls
+      * are potentially ambiguous and may cause a {@link 
SubspaceNotSpecifiedException} to be thrown
+      * at {@linkplain GridCoverage#render(GridExtent) rendering} time.</p>
+      *
+      * @return whether this dimensionality reduction is a slice in the source 
coverage.
+      *
+      * @see #getSliceCoordinates()
+      * @see #withSlicePoint(long[])
+      * @see #withSliceByRatio(double)
+      */
+     public boolean isSlice() {
+         return indexOfNonSlice() >= 0;
+     }
+ 
+     /**
+      * Ensures that {@link #isSlice()} returns {@code true}.
+      *
+      * @throws SubspaceNotSpecifiedException if this dimensionality reduction 
is not a slice of the source coverage.
+      */
+     final void ensureIsSlice() throws SubspaceNotSpecifiedException {
+         final int dim = indexOfNonSlice();
+         if (dim >= 0) {
+             throw new 
SubspaceNotSpecifiedException(Resources.format(Resources.Keys.AmbiguousGridAxisOmission_1,
 dim));
+         }
+     }
+ 
+     /**
+      * If {@link #isSlice()} would returns {@code false}, returns the index 
of the problematic dimension.
+      * Otherwise returns -1. This is used for more detailed error message.
+      */
+     private int indexOfNonSlice() {
+         int i = gridAxesToPass.length - 1;
+         final GridExtent extent = sourceGeometry.getExtent();
+         for (int dim = extent.getDimension(); --dim >= 0;) {
+             if (i >= 0 && dim == gridAxesToPass[i]) {
+                 i--;
+             } else if (!sliceCoordinates.containsKey(dim)) {
+                 if (extent.getLow(dim) != extent.getHigh(dim)) {
+                     return dim;
+                 }
+             }
+         }
+         return -1;
+     }
+ 
+     /**
+      * Returns {@code true} if the given grid geometry is likely to be 
already reduced.
+      * Current implementation checks only the number of dimensions.
+      *
+      * @param  candidate  the grid geometry to test.
+      * @return whether the given grid geometry is likely to be already 
reduced.
+      */
+     public boolean isReduced(final GridGeometry candidate) {
+         int dim;
+         if (candidate.extent == null && candidate.gridToCRS == null) {
+             dim = reducedGeometry.getTargetDimension();
+         } else {
+             dim = reducedGeometry.getDimension();
+         }
+         return candidate.getDimension() == dim;
+     }
+ 
+     /**
+      * Returns the grid geometry with only the retained grid axis dimension.
+      * The number of CRS dimensions should be reduced as well,
+      * but not necessarily in a one-to-one relationship.
+      *
+      * @return the grid geometry with retained grid dimensions.
+      */
+     public GridGeometry getReducedGridGeometry() {
+         return reducedGeometry;
+     }
+ 
+     /**
+      * Returns the grid geometry with all grid axis dimension.
+      * This is the {@code source} argument given to factory methods.
+      *
+      * @return the grid geometry with all grid dimensions.
+      */
+     public GridGeometry getSourceGridGeometry() {
+         return sourceGeometry;
+     }
+ 
+     /**
+      * Returns the part of the "grid to CRS" transform which has been removed 
in the reduced grid geometry.
+      * This is a pass-through transform (potentially, but not necessarily, 
implemented
+      * by {@link 
org.apache.sis.referencing.operation.transform.PassThroughTransform}).
+      * The number of source dimensions is the same than in the source grid 
geometry.
+      * The dimensions that are passed-through are the dimensions on which the 
reduced grid geometry operates.
+      *
+      * @param  anchor  the cell part to map (center or corner).
+      * @return removed part of the conversion from grid coordinates to "real 
world" coordinates.
+      */
+     private MathTransform getRemovedGridToCRS(final PixelInCell anchor) {
+         if (PixelInCell.CELL_CENTER.equals(anchor)) {
+             return removedGridToCRS;
+         } else if (PixelInCell.CELL_CORNER.equals(anchor)) {
+             return removedCornerToCRS;
+         }  else {
+             return PixelTranslation.translate(removedGridToCRS, 
PixelInCell.CELL_CENTER, anchor);
+         }
+     }
+ 
+     /**
+      * Returns the indices of the source dimensions that are kept in the 
reduced grid geometry.
+      *
+      * @return indices of source grid dimensions that are retained in the 
reduced grid geometry.
+      */
+     public int[] getSelectedDimensions() {
+         return gridAxesToPass.clone();
+     }
+ 
+     /**
+      * Returns the grid coordinates used in {@code reverse(…)} method calls 
for reconstituting some removed dimensions.
+      * Keys are indices of grid dimensions in the source that are 
<em>not</em> retained in the reduced grid geometry.
+      * Values are grid coordinates to assign to those dimensions when a 
{@code reverse(…)} method is executed.
+      *
+      * <p>This map does not need to contain an entry for all removed 
dimensions.
+      * If no slice point is specified for a given dimension, then the {@code 
reverse(…)} methods will use the
+      * full range of grid coordinates specified in the {@linkplain 
#getSourceGridGeometry() source geometry}.
+      * Often, those ranges have a {@linkplain GridExtent#getSize(int) size} 
of 1,
+      * in which case methods such as {@link GridCoverage#render(GridExtent)} 
will work anyway.
+      * If a removed source grid dimension had a size greater than 1 and no 
slice coordinates is specified;
+      * then the {@code reverse(…)} methods in this class will still work but 
an
+      * {@link SubspaceNotSpecifiedException} may be thrown later by other 
classes.</p>
+      *
+      * <p>This map is initially empty. Slice coordinates can be specified by 
calls
+      * to {@link #withSlicePoint(long[])} or {@link 
#withSliceByRatio(double)}.</p>
+      *
+      * @return source grid coordinates of the slice point used in {@code 
reverse(…)} method calls.
+      *
+      * @see #withSlicePoint(long[])
+      * @see #withSliceByRatio(double)
+      * @see GridExtent#getSliceCoordinates()
+      * @see GridCoverage.Evaluator#setDefaultSlice(Map)
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")     // Map is 
immutable.
+     public Map<Integer,Long> getSliceCoordinates() {
+         return sliceCoordinates;
+     }
+ 
+     /**
+      * Returns the dimension in the reduced grid for the given dimension in 
the source grid.
+      * If the specified source grid dimension is not retained, then this 
method returns a negative number.
+      *
+      * @param  dim  source dimension index to map to reduced dimension index.
+      * @return reduced dimension index, or a negative value if not mapped.
+      */
+     final int toReducedDimension(final int dim) {
+         return Arrays.binarySearch(gridAxesToPass, dim);
+     }
+ 
+     /**
+      * Returns the dimension in the source grid for the given dimension in 
the reduced grid.
+      *
+      * @param  dim  reduced dimension index to map to source dimension index.
+      * @return source dimension index.
+      * @throws IndexOutOfBoundsException if the given dimension is invalid.
+      */
+     final int toSourceDimension(final int dim) {
+         return gridAxesToPass[dim];
+     }
+ 
+     /**
+      * Returns the dimension in the source CRS for given counter of removed 
dimension.
+      *
+      * @param  i  0 for the first removed CRS dimension, 1 fo the second 
removed CRS dimension, <i>etc.</i>
+      * @return dimension in the source CRS which has been removed, of -1 if 
<var>i</var> is above bounds.
+      */
+     private int toRemovedDimension(final int i) {
+         return (i < crsAxesToRemove.length) ? crsAxesToRemove[i] : -1;
+     }
+ 
+     /**
+      * Ensures that {@code source} has the same number of dimensions and the 
same axes than {@code expected}.
+      * Only axis names that are specified in both extents are compared.
+      * If the {@code source} to validate is null, it defaults to the {@code 
expected} extent.
+      *
+      * @param  expected  grid extent with expected axes, or {@code null} if 
none.
+      * @param  source    grid extent to validate, or {@code null} if 
unspecified.
+      * @return whether the two extents are equal.
+      * @throws IllegalArgumentException if the number of dimensions or at 
least one axis name does not match.
+      */
+     private static boolean ensureSameAxes(final GridExtent expected, final 
GridExtent source) {
+         if (source == null) {
+             return true;
+         }
+         if (expected != null) {
+             expected.ensureSameAxes(source, "source");
+             if (expected.equals(source, ComparisonMode.IGNORE_METADATA)) {
+                 return true;
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns {@code true} if the {@code actual} CRS is equal, ignore 
metadata, to the one in {@code expected}.
+      * If any CRS is null, this method conservatively returns {@code true}.
+      * This is used for assertions only.
+      */
+     private static boolean assertSameCRS(final GridGeometry expected, final 
CoordinateReferenceSystem actual) {
+         if (actual != null && expected.isDefined(GridGeometry.CRS)) {
+             final var crs = expected.getCoordinateReferenceSystem();
+             if (crs != null) {
+                 return Utilities.deepEquals(crs, actual, 
ComparisonMode.DEBUG);
+             }
+         }
+         return true;
+     }
+ 
+     /**
+      * Returns a coordinate tuple on which dimensionality reduction has been 
applied.
+      * The coordinate reference system of the given {@code source} should be 
either
+      * null or equal (ignoring metadata) to the CRS of the source grid 
geometry.
+      * For performance reason, this is not verified unless assertions are 
enabled.
+      *
+      * @param  source  the source coordinate tuple, or {@code null}.
+      * @return the reduced coordinate tuple, or {@code null} if the given 
source was null.
+      */
+     @ArgumentCheckByAssertion
+     public DirectPosition apply(final DirectPosition source) {
+         if (source != null) {
+             ArgumentChecks.ensureDimensionMatches("source", 
sourceGeometry.getTargetDimension(), source);
+             assert assertSameCRS(sourceGeometry, 
source.getCoordinateReferenceSystem()) : source;
+             if (!isIdentity()) {
+                 final var reduced = new 
GeneralDirectPosition(reducedGeometry.getTargetDimension());
+                 /*
+                  * Following code is more complicated than what it could be 
if we stored a
+                  * `crsAxesToPass` array in this object. But it may not be 
worth to store
+                  * such array only for this method.
+                  */
+                 int dim = -1, remCounter = 0, removedAxis = 
crsAxesToRemove[0];
+                 for (int i=0; i < reduced.coordinates.length; i++) {
+                     while (++dim == removedAxis) {
+                         removedAxis = toRemovedDimension(++remCounter);
+                     }
+                     reduced.coordinates[i] = source.getOrdinate(dim);
+                 }
+                 return reduced;
+             }
+         }
+         return source;
+     }
+ 
+     /**
+      * Returns a grid extent on which dimensionality reduction has been 
applied.
+      * If the given source is {@code null}, then this method returns {@code 
null}.
+      * Nulls are accepted because they are valid argument values in calls to
+      * {@link GridCoverage#render(GridExtent)}.
+      *
+      * @param  source  the grid extent to reduce, or {@code null}.
+      * @return the reduced grid extent. May be {@code source}, which may be 
null.
+      * @throws MismatchedDimensionException if the given source does not have 
the expected number of dimensions.
+      * @throws IllegalArgumentException if axis types are specified but 
inconsistent in at least one dimension.
+      *
+      * @see GridExtent#selectDimensions(int...)
+      */
+     public GridExtent apply(final GridExtent source) {
+         if (source == null) return null;
+         if (ensureSameAxes(sourceGeometry.extent, source)) {
+             return reducedGeometry.extent;
+         }
+         return isIdentity() ? source : 
source.selectDimensions(gridAxesToPass);
+     }
+ 
+     /**
+      * Returns a grid geometry on which dimensionality reduction of the grid 
extent has been applied.
+      * It usually implies a reduction in the number of dimensions of the CRS 
as well,
+      * but not necessarily in same order.
+      *
+      * <p>If the given source is {@code null}, then this method returns 
{@code null}.
+      * Nulls are accepted because they are valid argument values in calls to
+      * {@link org.apache.sis.storage.GridCoverageResource#read(GridGeometry, 
int...)}.</p>
+      *
+      * @param  source  the grid geometry to reduce, or {@code null}.
+      * @return the reduced grid geometry. May be {@code source}, which may be 
null.
+      * @throws MismatchedDimensionException if the given source does not have 
the expected number of dimensions.
+      * @throws IllegalArgumentException if axis types are specified but 
inconsistent in at least one dimension.
+      *
+      * @see GridGeometry#selectDimensions(int...)
+      */
+     public GridGeometry apply(final GridGeometry source) {
+         if (source == null) return null;
+         if (ensureSameAxes(sourceGeometry.extent, source.extent)) {
+             return reducedGeometry;
+         }
+         return isIdentity() ? source : 
source.selectDimensions(gridAxesToPass);
+     }
+ 
+     /**
+      * Returns a grid coverage on which dimensionality reduction of the 
domain has been applied.
+      * This is a reduction in the number of dimensions of the grid extent. It 
usually implies a
+      * reduction in the number of dimensions of the CRS as well, but not 
necessarily in same order.
+      * The sample dimensions (coverage range) are unmodified.
+      *
+      * <p>The returned coverage is a <em>view</em>: changes in the source 
coverage
+      * are reflected immediately in the reduced coverage, and conversely.</p>
+      *
+      * <h4>Reversibility</h4>
+      * If {@link #isSlice()} returns {@code false},
+      * then the results of {@link #reverse(GridExtent)} are ambiguous
+      * and calls to {@link GridCoverage#render(GridExtent)} may cause
+      * an {@link SubspaceNotSpecifiedException} to be thrown.
+      * Unless the specified {@code source} grid coverage knows how to handle 
those cases.
+      *
+      * @param  source  the grid coverage to reduce.
+      * @return the reduced grid coverage, or {@code source} if this object 
{@linkplain #isIdentity() is identity}.
+      * @throws MismatchedDimensionException if the given source does not have 
the expected number of dimensions.
+      * @throws IllegalArgumentException if axis types are specified but 
inconsistent in at least one dimension.
+      *
+      * @see GridCoverageProcessor#selectGridDimensions(GridCoverage, int...)
+      */
+     @Override
+     public GridCoverage apply(final GridCoverage source) {
+         ArgumentChecks.ensureNonNull("source", source);
+         ensureSameAxes(sourceGeometry.extent, 
source.getGridGeometry().extent);
+         return isIdentity() ? source : new ReducedGridCoverage(source, this);
+     }
+ 
+     /**
+      * Returns a grid extent on which dimensionality reduction has been 
reverted.
+      * For all dimensions that were removed, grid coordinates will be set to 
the
+      * {@linkplain #getSliceCoordinates() slice coordinates} if specified,
+      * or to the original source grid coordinates otherwise.
+      * In the latter case, the reconstituted grid coordinates will be a 
single value
+      * if {@link #isSlice()} returns {@code true} (in which case the returned 
extent
+      * is unambiguous), or may be a (potentially ambiguous) range of values 
otherwise.
+      *
+      * <h4>Handling of null grid geometry</h4>
+      * If the given extent is {@code null}, then this method returns an extent
+      * with {@linkplain #getSliceCoordinates() slice coordinates} if they are 
known.
+      * If no slice coordinate has been specified, then this method returns 
{@code null}.
+      * Nulls are accepted because they are valid argument values
+      * in calls to {@link GridCoverage#render(GridExtent)}.
+      *
+      * @param  reduced  the reduced grid extent to revert, or {@code null}.
+      * @return the source grid extent. May be {@code reduced}, which may be 
null.
+      * @throws IncompleteGridGeometryException if the source grid geometry 
has no extent.
+      * @throws MismatchedDimensionException if the given extent does not have 
the expected number of dimensions.
+      * @throws IllegalArgumentException if axis types are specified but 
inconsistent in at least one dimension.
+      */
+     public GridExtent reverse(final GridExtent reduced) {
+         if (ensureSameAxes(reducedGeometry.extent, reduced)) {      // 
Argument validation.
+             if (sliceCoordinates.isEmpty()) {                       // Must 
be after argument validation.
+                 return sourceGeometry.extent;
+             }
+         }
+         if (isIdentity()) {
+             return reduced;
+         }
+         final GridExtent source = sourceGeometry.getExtent();
+         final long[] coordinates = source.getCoordinates();
+         final int m = coordinates.length >>> 1;
+         sliceCoordinates.forEach((dim, slice) -> {
+             coordinates[dim  ] = slice;
+             coordinates[dim+m] = slice;
+         });
+         if (reduced != null) {
+             for (int i=0; i < gridAxesToPass.length; i++) {
+                 final int dim = gridAxesToPass[i];
+                 coordinates[dim]   = reduced.getLow (i);
+                 coordinates[dim+m] = reduced.getHigh(i);
+             }
+         }
+         return new GridExtent(source, coordinates);
+     }
+ 
+     /**
+      * Returns a grid geometry on which dimensionality reduction has been 
reverted.
+      * For all dimensions that were removed, grid coordinates will be set to 
the
+      * {@linkplain #getSliceCoordinates() slice coordinates} if specified,
+      * or to the original source grid coordinates otherwise.
+      * In the latter case, the reconstituted dimensions will map a single 
coordinate value
+      * if {@link #isSlice()} returns {@code true} (in which case the returned 
grid geometry
+      * is unambiguous), or may map a (potentially ambiguous) range of grid 
coordinate values otherwise.
+      *
+      * <h4>Handling of null grid geometry</h4>
+      * If the given geometry is {@code null}, then this method returns a grid 
geometry
+      * with the {@linkplain #getSliceCoordinates() slice coordinates} if they 
are known.
+      * If no slice coordinate has been specified, then this method returns 
{@code null}.
+      * Nulls are accepted because they are valid argument values in calls to
+      * {@link org.apache.sis.storage.GridCoverageResource#read(GridGeometry, 
int...)}.
+      *
+      * @param  reduced  the reduced grid geometry to revert, or {@code null}.
+      * @return the source grid geometry. May be {@code reduced}, which may be 
null.
+      * @throws IncompleteGridGeometryException if the source grid geometry 
has no extent.
+      * @throws MismatchedDimensionException if the given geometry does not 
have the expected number of dimensions.
+      * @throws IllegalArgumentException if axis types are specified but 
inconsistent in at least one dimension.
+      */
+     public GridGeometry reverse(final GridGeometry reduced) {
+         final GridExtent extent = (reduced != null) ? reduced.extent : null;
+         if (ensureSameAxes(reducedGeometry.extent, extent)) {       // 
Argument validation.
+             if (sliceCoordinates.isEmpty()) {                       // Must 
be after argument validation.
+                 return sourceGeometry;
+             }
+         }
+         if (isIdentity()) {
+             return reduced;
+         }
+         /*
+          * Build a compound CRS on a "best effort" basis. This operation is 
costly
+          * if `fullCRS(…)` must be invoked, so we try to use the existing CRS 
first.
+          */
+         CoordinateReferenceSystem crs = null;
+         if (reduced.isDefined(GridGeometry.CRS) && 
sourceGeometry.isDefined(GridGeometry.CRS)) {
+             final CoordinateReferenceSystem reducedCRS = 
reduced.getCoordinateReferenceSystem();
+             if 
(Utilities.equalsIgnoreMetadata(reducedGeometry.getCoordinateReferenceSystem(), 
reducedCRS)) {
+                 crs = sourceGeometry.getCoordinateReferenceSystem();
+             } else {
+                 FactoryException cause = null;
+                 try {
+                     crs = fullCRS(reducedCRS);
+                 } catch (FactoryException e) {
+                     cause = e;
+                 }
+                 if (crs == null) {
+                     throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions,
 cause));
+                 }
+             }
+         }
+         /*
+          * Build an envelope and a resolution array where values at each 
dimension are copied
+          * either from the source geometry or from the specified reduced 
geometry.
+          */
+         final int targetDim = sourceGeometry.getTargetDimension();
+         final double[] lowerCorner = new double[targetDim];
+         final double[] upperCorner = new double[targetDim];
+         final double[] resolution  = new double[targetDim];
+         long nonLinears  = 0;
+         int  reducedDim  = 0;
+         int  remCounter  = 0;
+         int  removedAxis = crsAxesToRemove[0];
+         for (int i=0; i<targetDim; i++) {
+             final GridGeometry source;
+             final int dim;
+             if (i == removedAxis) {
+                 dim = removedAxis;
+                 source = sourceGeometry;
+                 removedAxis = toRemovedDimension(++remCounter);     // 
Dimension of the next axis to remove.
+             } else {
+                 dim = reducedDim++;
+                 source = reduced;
+             }
+             lowerCorner[i] =  source.envelope.getLower(dim);
+             upperCorner[i] =  source.envelope.getUpper(dim);
+             resolution [i] = (source.resolution != null) ? 
source.resolution[dim] : Double.NaN;
+             if ((source.nonLinears & Numerics.bitmask(dim)) != 0) {
+                 nonLinears |= Numerics.bitmask(i);
+             }
+         }
+         return new GridGeometry(reverse(extent),
+                 fullGridToCRS(reduced, PixelInCell.CELL_CENTER),
+                 fullGridToCRS(reduced, PixelInCell.CELL_CORNER),
+                 new ImmutableEnvelope(lowerCorner, upperCorner, crs),
+                 ArraysExt.allEquals(resolution, Double.NaN) ? null : 
resolution,
+                 nonLinears);
+     }
+ 
+     /**
+      * Builds a CRS with the same number of axes than the CRS of the source 
geometry.
+      * Axes are copied either from the source CRS or the reduced CRS, 
depending on
+      * whether the corresponding dimension is present in the reduced CRS.
+      *
+      * @param  reduced  the CRS to inflate to the same number of dimensions 
than the source CRS.
+      * @return the "inflated" CRS.
+      */
+     private CoordinateReferenceSystem fullCRS(final CoordinateReferenceSystem 
reduced) throws FactoryException {
+         if (componentsOfCRS == null) {
+             return null;
+         }
+         final var components = new 
CoordinateReferenceSystem[componentsOfCRS.length];
+         int lower = 0;
+         for (int i=0; i<components.length; i++) {
+             final Object element = componentsOfCRS[i];
+             if (element instanceof CoordinateReferenceSystem) {
+                 components[i] = (CoordinateReferenceSystem) element;
+             } else {
+                 final int upper = lower + (Integer) element;
+                 components[i] = CRS.getComponentAt(reduced, lower, upper);
+                 lower = upper;
+             }
+         }
+         return CRS.compound(components);
+     }
+ 
+     /**
+      * Builds a transform with the same number of dimensions that the 
transform of the source geometry.
+      *
+      * @param  reduced  the transform to inflate to the same number of 
dimensions that the source geometry.
+      * @param  anchor   whether the transform map pixel centers or pixel 
corners.
+      * @return the "inflated" transform.
+      */
+     private MathTransform fullGridToCRS(final GridGeometry reduced, final 
PixelInCell anchor) {
+         final MathTransform removed = getRemovedGridToCRS(anchor);
+         if (removed == null || !reduced.isDefined(GridGeometry.GRID_TO_CRS)) {
+             return null;
+         }
+         MathTransform gridToCRS = reduced.getGridToCRS(anchor);
+         if 
(Utilities.equalsIgnoreMetadata(reducedGeometry.getGridToCRS(anchor), 
gridToCRS)) {
+             return sourceGeometry.getGridToCRS(anchor);
+         }
+         gridToCRS = MathTransforms.passThrough(gridAxesToPass, gridToCRS, 
removed.getTargetDimensions());
+         return MathTransforms.concatenate(removed, gridToCRS);
+     }
+ 
+     /**
+      * Returns a dimensional reduction which will use the given source grid 
indices for {@code reverse(…)} operations.
+      * The length of the given {@code slicePoint} array shall be the number 
of dimensions of the source grid geometry.
+      * All given coordinate values shall be inside the source grid extent.
+      *
+      * @param  point  grid coordinates of a point located on the slice.
+      * @return the dimensionality reduction with the given slice point used 
for reverse operations.
+      * @throws IncompleteGridGeometryException if the source grid geometry 
has no extent.
+      * @throws MismatchedDimensionException if the given point does not have 
the expected number of dimensions.
+      * @throws PointOutsideCoverageException if the given point is outside 
the source grid extent.
+      */
+     public DimensionalityReduction withSlicePoint(final long[] point) {
+         ArgumentChecks.ensureNonNull("point", point);
+         final GridExtent extent = sourceGeometry.getExtent();
+         final int sourceDim = extent.getDimension();
 -        ArgumentChecks.ensureDimensionMatches("slicePoint", sourceDim, 
extent);
+         final Map<Integer,Long> slices = new HashMap<>();
+         for (int dim=0; dim < sourceDim; dim++) {
+             final long low   = extent.getLow (dim);
+             final long high  = extent.getHigh(dim);
+             final long value = point[dim];
+             if (value < low || value > high) {
+                 String b = Arrays.toString(point);
+                 b = b.substring(1, b.length() - 1);   // Remove brackets.
+                 throw new PointOutsideCoverageException(Resources.format(
+                         Resources.Keys.GridCoordinateOutsideCoverage_4,
+                         extent.getAxisIdentification(dim,dim), low, high, b));
+             }
+             if (low != high && toReducedDimension(dim) < 0) {
+                 slices.put(dim, value);
+             }
+         }
+         return slices.equals(sliceCoordinates) ? this : new 
DimensionalityReduction(this, slices);
+     }
+ 
+     /**
+      * Returns a dimensional reduction with a relative slice position
+      * for every grid dimensions that have been removed.
+      * The relative position is specified by a ratio between 0 and 1 where
+      * 0 maps to {@linkplain GridExtent#getLow(int) low} grid coordinates,
+      * 1 maps to {@linkplain GridExtent#getHigh(int) high grid coordinates} 
and
+      * 0.5 maps to the median position.
+      *
+      * @param  ratio  the ratio to apply on all removed grid dimensions.
+      * @return the dimensionality reduction with the given slice ratio 
applied.
+      * @throws IncompleteGridGeometryException if the source grid geometry 
has no extent.
+      * @throws IllegalArgumentException if the given ratio is not between 0 
and 1 inclusive.
+      *
+      * @see GridExtent#getRelative(int, double)
+      * @see GridDerivation#sliceByRatio(double, int...)
+      */
+     public DimensionalityReduction withSliceByRatio(final double ratio) {
+         ArgumentChecks.ensureBetween("ratio", 0, 1, ratio);
+         final GridExtent extent = sourceGeometry.getExtent();
+         final int sourceDim = extent.getDimension();
+         final Map<Integer,Long> slices = new HashMap<>();
+         for (int dim=0; dim < sourceDim; dim++) {
+             if (toReducedDimension(dim) < 0 && extent.getLow(dim) != 
extent.getHigh(dim)) {
+                 slices.put(dim, extent.getRelative(dim, ratio));
+             }
+         }
+         return slices.equals(sliceCoordinates) ? this : new 
DimensionalityReduction(this, slices);
+     }
+ }
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
index cf89e64a46,307800bd33..d5974b88fd
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
@@@ -24,10 -26,6 +24,10 @@@ import org.apache.sis.util.ArgumentChec
   * A view over the low or high grid envelope coordinates.
   * This is not a general-purpose grid coordinates since it assumes a {@link 
GridExtent} coordinates layout.
   *
-  * <div class="note"><b>Upcoming API generalization:</b>
++ * <h2>Upcoming API generalization</h2>
 + * this class may implement the {@code GridCoordinates} interface in a future 
Apache SIS version.
-  * This is pending GeoAPI update.</div>
++ * This is pending GeoAPI update.
 + *
   * @author  Martin Desruisseaux (Geomatys)
   * @version 1.1
   * @since   1.0
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java
index 6690fca29a,2aeb42d671..6805ddfadf
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java
@@@ -22,11 -22,11 +22,10 @@@ import java.util.LinkedHashSet
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.internal.util.CollectionsExt;
  import org.apache.sis.internal.util.UnmodifiableArrayList;
- import org.apache.sis.util.resources.Errors;
  
  // Branch-dependent imports
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.LogicalOperatorName;
 +import org.apache.sis.internal.geoapi.filter.LogicalOperator;
 +import org.apache.sis.internal.geoapi.filter.LogicalOperatorName;
  
  
  /**
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CommonDomainFinder.java
index 0000000000,4fa3b4b157..8ff67d02f1
mode 000000,100644..100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CommonDomainFinder.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CommonDomainFinder.java
@@@ -1,0 -1,376 +1,385 @@@
+ /*
+  * 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.internal.coverage;
+ 
+ import java.util.Map;
+ import java.util.LinkedHashMap;
+ import java.util.NoSuchElementException;
+ import org.opengis.util.FactoryException;
+ import org.opengis.geometry.MismatchedDimensionException;
+ import org.opengis.referencing.datum.PixelInCell;
+ import org.opengis.referencing.operation.Matrix;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.TransformException;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.opengis.referencing.operation.NoninvertibleTransformException;
+ import org.apache.sis.referencing.CRS;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.coverage.grid.GridExtent;
+ import org.apache.sis.coverage.grid.GridGeometry;
+ import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+ import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
+ import org.apache.sis.internal.feature.Resources;
+ import org.apache.sis.internal.util.Numerics;
+ import org.apache.sis.util.Numbers;
+ 
+ 
+ /**
+  * Helper class for building a combined domain from a list of grid geometries.
+  * After construction, one of the following methods shall be invoked exactly 
once.
+  *
+  * <ul>
+  *   <li>{@link #setFromGridAligned(GridGeometry...)}</li>
+  * </ul>
+  *
+  * Then, the result can be obtained by the given methods:
+  *
+  * <ul>
+  *   <li>{@link #result()}</li>
+  *   <li>{@link #gridTranslations()}</li>
+  *   <li>{@link #sourceOfGridToCRS()}</li>
+  * </ul>
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.4
+  * @since   1.4
+  */
+ public final class CommonDomainFinder {
+     /**
+      * Whether to compute intersection (true) or union (false) of all grid 
coverages.
+      * This is not yet configurable in current version.
+      *
+      * <p>We use this constant for tracking the code to update when we will 
want to provide an option for using
+      * union or strict equality instead (the latter would be a mode that 
fails if all extents are not identical).
+      * Note that in the case of unions, it would be possible to specify 
coverages with no intersection.
+      * Whether we should accept that or raise an exception is still an open 
question.</p>
+      */
+     public static final boolean INTERSECTION = true;
+ 
+     /**
+      * The grid geometry taken as the reference for computing the 
translations of all grid geometry items.
+      * Values of {@link #gridTranslations} and {@link #itemTranslations} are 
relative to that reference.
+      * At first this is an arbitrary grid geometry, for example the first 
encountered one.
+      * After {@code CommonDomainFinder} completed its task, this is updated 
to the final result.
+      *
+      * @see #result()
+      */
+     private GridGeometry reference;
+ 
+     /**
+      * Coordinate reference system of the reference, or {@code null} if not 
yet known.
+      * It will be set to the CRS of the first grid geometry where the CRS is 
defined.
+      */
+     private CoordinateReferenceSystem crs;
+ 
+     /**
+      * The inverse of the "grid to CRS" transform of the grid geometry taken 
as a reference.
+      */
+     private MathTransform crsToGrid;
+ 
+     /**
+      * The convention to use for fetching the "grid to CRS" transforms.
+      */
+     private final PixelInCell anchor;
+ 
+     /**
+      * The combined extent, as the union or intersection of all grid extents.
+      */
+     private GridExtent extent;
+ 
+     /**
+      * The translation in units of grid cells from the {@linkplain 
#reference} grid geometry
+      * to the grid geometry in the key.
+      */
+     private final Map<GridGeometry,long[]> gridTranslations;
+ 
+     /**
+      * Translations in units of grid cells from the {@linkplain #reference} 
grid geometry to each item.
+      * For each index <var>i</var>, {@code itemTranslations[i]} is a value 
from {@link #gridTranslations}
+      * map and may be reused at more than one index <var>i</var>.
+      *
+      * @see #gridTranslations()
+      */
+     private long[][] itemTranslations;
+ 
+     /**
+      * If one of the grid geometries has the same "grid to CRS" than the 
common grid geometry, the index.
+      * Otherwise -1.
+      */
+     private int sourceOfGridToCRS;
+ 
+     /**
+      * Creates a new common domain finder.
+      *
+      * @param  anchor  the convention to use for fetching the "grid to CRS" 
transforms.
+      */
+     CommonDomainFinder(final PixelInCell anchor) {
+         this.anchor = anchor;
+         gridTranslations = new LinkedHashMap<>();
+     }
+ 
+     /**
+      * Computes a common grid geometry from the given items.
+      * All items shall share be aligned on the same grid.
+      * Items may be translated relative to each other,
+      * but the translations shall be an integer number of grid cells.
+      *
+      * <h4>Coordinate reference system</h4>
+      * If the CRS of a grid geometry is undefined, it is assumed the same 
than other grid geometries.
+      *
+      * @param  items  the grid geometries for which to compute a common grid 
geometry.
+      * @throws IllegalGridGeometryException if the specified item is not 
compatible with the reference grid geometry.
+      */
+     final void setFromGridAligned(final GridGeometry... items) {
+         itemTranslations = new long[items.length][];
+         for (int i=0; i<items.length; i++) {
+             itemTranslations[i] = gridTranslations.computeIfAbsent(items[i], 
this::itemToCommon);
+         }
+         /*
+          * Change the reference grid geometry for matching more closely the 
desired grid extent.
+          * If one item has exactly the desired grid extent, use it. Otherwise 
search for an item
+          * having the same origin. This criterion is arbitrary and may change 
in future version.
+          */
+         GridGeometry fallback = null;
+         GridExtent   location = null;
+         for (final Map.Entry<GridGeometry,long[]> entry : 
gridTranslations.entrySet()) {
+             final GridGeometry item   = entry.getKey();
+             final GridExtent actual   = item.getExtent();
+             final GridExtent expected = extent.translate(entry.getValue());
+             if (actual.equals(expected)) {
+                 setGridToCRS(items, item);      // Must be before `reference` 
assignation.
+                 reference = item;
+                 return;
+             }
+             // Arbitrary criterion (may be revisited in any future version).
 -            if (fallback == null && 
expected.getLow().equals(actual.getLow())) {
++            if (fallback == null && sameLow(expected, actual)) {
+                 location = expected;
+                 fallback = item;
+             }
+         }
+         if (fallback == null) {
+             fallback = reference;
+             location = extent;
+         }
+         setGridToCRS(items, fallback);
+         try {
+             reference = fallback.relocate(location);
+         } catch (TransformException e) {
+             throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries),
 e);
+         }
+     }
+ 
++    private static boolean sameLow(final GridExtent e1, final GridExtent e2) {
++        final int dim = e1.getDimension();
++        if (dim != e2.getDimension()) return false;
++        for (int i=0; i<dim; i++) {
++            if (e1.getLow(i) != e2.getLow(i)) return false;
++        }
++        return true;
++    }
++
+     /**
+      * Given a grid geometry with the desired "grid to CRS", saves its index 
in {@link #sourceOfGridToCRS}.
+      * This method updates all previously computed translations for making 
them relative to the new reference.
+      *
+      * Note: updating values of the {@link #gridTranslations} map indirectly 
update all
+      * {@link #itemTranslations} array elements.
+      */
+     private void setGridToCRS(final GridGeometry[] items, final GridGeometry 
item) {
+         sourceOfGridToCRS = indexOf(items, item);
+         if (item == reference) {                    // Quick check for a 
common case.
+             return;
+         }
+         final long[] oldReference = itemTranslations[indexOf(items, 
reference)];
+         final long[] newReference = itemTranslations[sourceOfGridToCRS];
+         final long[] change = new long[newReference.length];
+         for (int i=0; i < newReference.length; i++) {
+             change[i] = Math.subtractExact(newReference[i], oldReference[i]);
+         }
+         for (final long[] offset : gridTranslations.values()) {
+             for (int i=0; i < offset.length; i++) {
+                 offset[i] = Math.subtractExact(offset[i], change[i]);
+             }
+         }
+     }
+ 
+     /**
+      * Returns the index of the given grid geometry.
+      * This method is invoked when the instance should always exist in the 
array.
+      */
+     private static int indexOf(final GridGeometry[] items, final GridGeometry 
item) {
+         for (int i=0; i<items.length; i++) {
+             if (items[i] == item) {
+                 return i;
+             }
+         }
+         throw new NoSuchElementException();         // Should never happen.
+     }
+ 
+     /**
+      * Returns the common grid geometry computed from all specified items.
+      *
+      * @return a grid geometry which is the union or intersection of all 
specified items.
+      */
+     final GridGeometry result() {
+         if (crs != null && !reference.isDefined(GridGeometry.CRS)) {
+             reference = new GridGeometry(extent, anchor, 
reference.getGridToCRS(anchor), crs);
+         }
+         return reference;
+     }
+ 
+     /**
+      * Returns the translations (in units of grid cells) from the common grid 
geometry to all items.
+      * The items are the arguments given to {@link 
#setFromGridAligned(GridGeometry...)}, in order.
+      * The common grid geometry is the value returned by {@link #result()}.
+      *
+      * <p>The returned array should not be modified because it is not 
cloned.</p>
+      *
+      * @return translation from the common grid geometry to all items. This 
array is not cloned.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     final long[][] gridTranslations() {
+         return itemTranslations;
+     }
+ 
+     /**
+      * If one items has the same "grid to CRS" than the common grid geometry, 
returns its index.
+      *
+      * @return index of an items having the desired "grid to CRS", or -1 if 
none.
+      */
+     final int sourceOfGridToCRS() {
+         return sourceOfGridToCRS;
+     }
+ 
+     /**
+      * Computes the translation in units of grid cells from the common grid 
geometry to the given item.
+      * This method also opportunistically computes the union or intersection 
of all grid extents.
+      *
+      * @param  item  the grid geometry for which to get a translation from 
the common grid geometry.
+      * @return translation in unis of grid cells. Note that the caller may 
reuse this array for many grid geometries.
+      * @throws IllegalGridGeometryException if the specified item is not 
compatible with the reference grid geometry.
+      */
+     private long[] itemToCommon(final GridGeometry item) {
+         /*
+          * Compute the change ourselves instead of invoking 
`GridGeometry.createTransformTo(…)`
+          * because we do not want wraparound handling when we search for a 
simple translation.
+          */
+         MathTransform change = item.getGridToCRS(anchor);
+         try {
+             if (crsToGrid == null) {
+                 crsToGrid = change.inverse();
+                 reference = item;
+             }
+             if (item.isDefined(GridGeometry.CRS)) {
+                 final CoordinateReferenceSystem src = 
item.getCoordinateReferenceSystem();
+                 if (crs == null) {
+                     crs = src;
+                 } else {
+                     /*
+                      * Ask for a change of CRS without specifying an area of 
interest (AOI) on the assumption
+                      * that if the transformation is only a translation, the 
AOI would not make a difference.
+                      * It save not only the AOI computation cost, but also 
make easier for `findOperation(…)`
+                      * to use its cache.
+                      */
+                     change = MathTransforms.concatenate(change, 
CRS.findOperation(src, crs, null).getMathTransform());
+                 }
+             }
+             change = MathTransforms.concatenate(change, crsToGrid);
+         } catch (FactoryException | NoninvertibleTransformException | 
MismatchedDimensionException e) {
+             throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries),
 e);
+         }
+         final long[] offset = 
integerTranslation(MathTransforms.getMatrix(change), null);
+         if (offset == null) {
+             throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries));
+         }
+         /*
+          * The grid geometry has been accepted as valid. Now compute the 
combined extent,
+          * taking offset in account. At this point this is an offset TO the 
common grid geometry.
+          * It will be converted to an offset FROM the common grid geometry 
after the extent update.
+          */
+         final GridExtent e = item.getExtent().translate(offset);
+         if (extent == null) {
+             extent = e;
+         } else if (INTERSECTION) {
+             extent = extent.intersect(e);
+         } else if (!extent.equals(e)) {
+             throw new IllegalGridGeometryException();
+         }
+         for (int i=0; i<offset.length; i++) {
+             offset[i] = Math.negateExact(offset[i]);
+         }
+         return offset;
+     }
+ 
+     /**
+      * If the given matrix is the identity matrix except for translation 
terms, returns the translation.
+      * Translation terms must be integer values and will be stored in the 
given {@code offset} array.
+      * If the matrix is not an integer translation, this method return {@code 
null} without modifying
+      * the given {@code offset} array.
+      *
+      * @param  change  conversion between two grid geometries, or {@code 
null}.
+      * @param  offset  where to store translation terms if the change is an 
integer translation, or {@code null}.
+      * @return the translation terms, or {@code null} if the given matrix 
does not met the conditions.
+      *
+      * @see 
org.apache.sis.referencing.operation.matrix.Matrices#isTranslation(Matrix)
+      */
+     public static long[] integerTranslation(final Matrix change, long[] 
offset) {
+         if (change == null) {
+             return null;
+         }
+         final int numRows = change.getNumRow();
+         final int numCols = change.getNumCol();
+         for (int j=0; j<numRows; j++) {
+             for (int i=0; i<numCols; i++) {
+                 double tolerance = Numerics.COMPARISON_THRESHOLD;
+                 double e = change.getElement(j, i);
+                 if (i == j) {
+                     e--;
+                 } else if (i == numCols - 1) {
+                     final double a = Math.abs(e);
+                     if (a > 1) {
+                         tolerance = Math.min(tolerance*a, 0.125);
+                     }
+                     e -= Math.rint(e);
+                 }
+                 if (!(Math.abs(e) <= tolerance)) {      // Use `!` for 
catching NaN.
+                     return null;
+                 }
+             }
+         }
+         /*
+          * Store the translation terms after we have determined that they are 
integers.
+          * It must be an "all or nothing" operation (unless an exception is 
thrown).
+          */
+         if (offset == null) {
+             offset = new long[numRows - 1];
+         }
+         final int i = numCols - 1;
+         if (change instanceof ExtendedPrecisionMatrix) {
+             final var epm = (ExtendedPrecisionMatrix) change;
+             for (int j=0; j<offset.length; j++) {
+                 final Number e = epm.getElementOrNull(j, i);
+                 offset[j] = (e != null) ? Numbers.round(e) : 0;
+             }
+         } else {
+             for (int j=0; j<offset.length; j++) {
+                 offset[j] = Math.round(change.getElement(j, i));
+             }
+         }
+         return offset;
+     }
+ }
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java
index 752414f502,9d46610846..f153e9e9dd
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java
@@@ -41,9 -43,9 +42,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.2
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   */
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
index 0000000000,565bc0af2d..ef4f109054
mode 000000,100644..100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
@@@ -1,0 -1,194 +1,194 @@@
+ /*
+  * 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.coverage.grid;
+ 
+ import org.opengis.referencing.crs.SingleCRS;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.opengis.referencing.datum.PixelInCell;
+ import org.opengis.referencing.operation.Matrix;
+ import org.apache.sis.internal.referencing.DirectPositionView;
+ import org.apache.sis.referencing.operation.matrix.Matrix3;
+ import org.apache.sis.referencing.operation.matrix.Matrix4;
+ import org.apache.sis.referencing.operation.matrix.Matrices;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.referencing.crs.HardCodedCRS;
+ import org.apache.sis.referencing.CRS;
+ import org.apache.sis.util.ComparisonMode;
+ import org.apache.sis.util.Utilities;
+ import org.apache.sis.test.TestCase;
+ import org.junit.Test;
+ 
 -import static org.opengis.test.Assert.*;
++import static org.apache.sis.test.Assert.*;
+ 
+ 
+ /**
+  * Tests {@link DimensionalityReduction}.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.4
+  * @since   1.4
+  */
+ public final class DimensionalityReductionTest extends TestCase {
+     /**
+      * Convenience method for building a grid geometry.
+      *
+      * @param  low        low grid coordinates, inclusive.
+      * @param  high       high grid coordinates, inclusive.
+      * @param  gridToCRS  map pixel corner to geographic coordinates.
+      * @param  crs        target of {@code gridToCRS}.
+      * @return grid geometry for testing purposes.
+      */
+     private static GridGeometry createGridGeometry(long[] low, long[] high, 
Matrix gridToCRS, CoordinateReferenceSystem crs) {
+         return new GridGeometry(new GridExtent(null, low, high, true), 
PixelInCell.CELL_CORNER,
+                                 MathTransforms.linear(gridToCRS), crs);
+     }
+ 
+     /**
+      * Creates a four-dimensional grid geometry to be used for the tests in 
this class.
+      * The "grid to CRS" transform is a linear one.
+      */
+     private static GridGeometry linearGrid() {
+         return createGridGeometry(new long[] { 20, 136,  4, -2},
+                                   new long[] {419, 201, 10,  2},
+                                   gridToCRS(), HardCodedCRS.GEOID_4D);
+     }
+ 
+     /**
+      * Returns a "grid to CRS" transform for the full grid geometry to be 
used in tests.
+      */
+     private static Matrix gridToCRS() {
+         return Matrices.create(5, 5, new double[] {
+                         0.5, 0,   0,  0,  -180,
+                         0,   0.5, 0,  0,   -90,
+                         0,   0,   5,  0,    -2,
+                         0,   0,   0,  7, 60000,
+                         0,   0,   0,  0,     1});
+     }
+ 
+     /**
+      * Returns the matrix for the "grid to CRS" transform of the test grid, 
but without height.
+      */
+     private static Matrix4 withHeightRemoved() {
+         return new Matrix4(0.5, 0,   0,  -180,
+                            0,   0.5, 0,   -90,
+                            0,   0,   7, 60000,
+                            0,   0,   0,     1);
+     }
+ 
+     /**
+      * Returns the matrix for the "grid to CRS" transform with only the 
horizontal part.
+      */
+     private static Matrix3 withHorizontal() {
+         return new Matrix3(0.5, 0,  -180,
+                            0,   0.5, -90,
+                            0,   0,     1);
+     }
+ 
+     /**
+      * Asserts that the "grid to CRS" transform of the given grid geometry is 
equal to the specified value.
+      *
+      * @param  test      the grid geometry to verify.
+      * @param  expected  the expected "grid to CRS" transform.
+      * @return the CRS of the given grid geometry.
+      */
+     private static CoordinateReferenceSystem verifyGridToCRS(final 
GridGeometry test, final Matrix expected) {
+         Matrix actual = 
MathTransforms.getMatrix(test.getGridToCRS(PixelInCell.CELL_CORNER));
+         assertMatrixEquals("gridToCRS", expected, actual, STRICT);
+         return test.getCoordinateReferenceSystem();
+     }
+ 
+     /**
+      * Tests reduction of a direct position.
+      *
+      * @param reduction  the reduction to apply.
+      * @param source     source coordinates.
+      * @param target     expected reduced coordinates.
+      */
+     private static void testPosition(final DimensionalityReduction reduction, 
double[] source, double[] target) {
+         assertArrayEquals(target, reduction.apply(new 
DirectPositionView.Double(source)).getCoordinate(), STRICT);
+     }
+ 
+     /**
+      * Tests the removal of a single dimension in the middle of the "grid to 
CRS" transform.
+      * This test use the same CRS for all steps.
+      */
+     @Test
+     public void testRemoval() {
+         var reduction = DimensionalityReduction.remove(linearGrid(), 2);      
// Remove height.
+         var crs = verifyGridToCRS(reduction.getReducedGridGeometry(), 
withHeightRemoved());
+         assertArrayEquals(new SingleCRS[] {HardCodedCRS.WGS84, 
HardCodedCRS.TIME},
+                           CRS.getSingleComponents(crs).toArray());
+         /*
+          * Tests the fast path in reduction and reverse operations.
+          */
+         assertSame(reduction.getReducedGridGeometry(), 
reduction.apply(linearGrid()));
+         assertSame(reduction.getSourceGridGeometry(),  
reduction.reverse(reduction.getReducedGridGeometry()));
+         /*
+          * Test the reverse operation with a slightly different "grid to CRS".
+          * It will test the generic path, as opposed to above-cited fast path.
+          */
+         GridGeometry test = reduction.reverse(createGridGeometry(
+                 new long[] {100, 180, -5},
+                 new long[] {200, 195, -3},
+                 new Matrix4(0.5, 0,   0,  -100,
+                             0,   0.5, 0,   -80,
+                             0,   0,   7, 60002,
+                             0,   0,   0,     1), crs));
+ 
+         crs = verifyGridToCRS(test, Matrices.create(5, 5, new double[] {
+                         0.5, 0,   0,  0,  -100,
+                         0,   0.5, 0,  0,   -80,
+                         0,   0,   5,  0,    -2,
+                         0,   0,   0,  7, 60002,
+                         0,   0,   0,  0,     1}));
+ 
+         
assertSame(reduction.getSourceGridGeometry().getCoordinateReferenceSystem(), 
crs);
+         testPosition(reduction, new double[] {100, 101, 102, 103}, new 
double[] {100, 101, 103});
+     }
+ 
+     /**
+      * Tests the selection of two dimensions.
+      */
+     @Test
+     public void testSelect() {
+         var reduction = DimensionalityReduction.select2D(linearGrid());
+         var gridToCRS = 
reduction.getReducedGridGeometry().getGridToCRS(PixelInCell.CELL_CORNER);
+         assertMatrixEquals("gridToCRS", withHorizontal(), 
MathTransforms.getMatrix(gridToCRS), STRICT);
+         assertSame(HardCodedCRS.WGS84, 
reduction.getReducedGridGeometry().getCoordinateReferenceSystem());
+ 
+         GridGeometry test = reduction.reverse(createGridGeometry(
+                 new long[] {380, 100},
+                 new long[] {400, 200},
+                 new Matrix3(0,   0.5, -80,
+                             0.5, 0,  -100,
+                             0,   0,     1), 
HardCodedCRS.WGS84_LATITUDE_FIRST));
+ 
+         var crs = verifyGridToCRS(test, Matrices.create(5, 5, new double[] {
+                         0,   0.5, 0,  0,   -80,
+                         0.5, 0,   0,  0,  -100,
+                         0,   0,   5,  0,    -2,
+                         0,   0,   0,  7, 60000,
+                         0,   0,   0,  0,     1}));
+         /*
+          * CRS should have different axis order.
+          */
+         var sourceCRS = 
reduction.getSourceGridGeometry().getCoordinateReferenceSystem();
+         assertFalse(Utilities.equalsIgnoreMetadata(sourceCRS, crs));
+         assertTrue(Utilities.deepEquals(test, test, 
ComparisonMode.ALLOW_VARIANT));     // Ignore axis order.
+         testPosition(reduction, new double[] {100, 101, 102, 103}, new 
double[] {100, 101});
+     }
+ }
diff --cc 
core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
index a716370dbf,00185fec10..0b692d6e3f
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
@@@ -285,6 -303,50 +290,50 @@@ public final class PassThroughTransform
                  1, 0, 0, 0, 0, 0,
                  0, 0, 0, 1, 0, 0,
                  0, 0, 0, 0, 1, 0,
 -                0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), 
null);
 +                0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), 
0);
      }
+ 
+     /**
+      * Tests the creation of a pass-through transform with modified 
coordinates that are not consecutive.
+      * This is a test of {@link PassThroughTransform#create(BitSet, 
MathTransform, int, MathTransformFactory)}
+      * factory method.
+      *
+      * @throws FactoryException if an error occurred while combining the 
transforms.
+      * @throws TransformException if a test coordinate tuple cannot be 
transformed.
+      */
+     @Test
+     public void testNonConsecutiveModifiedCoordinates() throws 
FactoryException, TransformException {
+         random = TestUtilities.createRandomNumberGenerator();
+         /*
+          * First, create a pass-through transform from an inseparable 
`PseudoTransform`.
+          * Because `PseudoTransform` is inseparable, the modified coordinates 
must be consecutive.
+          * However the `PassThroughTransform` result is partially separable 
and used in next step.
+          */
+         final var bitset = new BitSet();
+         bitset.set(1, 3, true);                                 // Modified 
coordinates = {1, 2}.
+         MathTransform subTransform = new PseudoTransform(2, 2);
+         transform = PassThroughTransform.create(bitset, subTransform, 5, 
null);
+         assertEquals(5, transform.getSourceDimensions());
+         assertEquals(5, transform.getTargetDimensions());
+         assertEquals(1, ((PassThroughTransform) 
transform).firstAffectedCoordinate);
+         assertEquals(2, ((PassThroughTransform) 
transform).numTrailingCoordinates);
+         isInverseTransformSupported = false;
+         verifyTransform(subTransform, 1);
+         /*
+          * Now test with non-consecutive coordinates, except for the 
`PseudoTransform` part
+          * which must still be in consecutive coordinates. We add a linear 
transform before
+          * for making the work a little bit harder.
+          */
+         bitset.clear();
+         bitset.set(1, true);
+         bitset.set(3, 5, true);     // The inseparable `PseudoTransform` part.
+         bitset.set(6, true);
+         bitset.set(9, true);
+         subTransform = MathTransforms.concatenate(MathTransforms.scale(4, 3, 
7, 5, -6), transform);
+         transform = PassThroughTransform.create(bitset, subTransform, 10, 
null);
+         verifyTransform(
+             // _____________[0]_________[1]-____[2]____[3]_______[4]     
Dimension index in sub-transform.
+             new double[] {2, 1, -1,    0.2,    0.1, 9,  2, 8, 4, -1},
+             new double[] {2, 4, -1, 1600.6, 2700.7, 9, 10, 8, 4,  6});
+     }
  }
diff --cc 
storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
index b100426b1c,4c3a19aae4..10f05d2c7e
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
@@@ -51,17 -51,15 +51,15 @@@ public class MemoryFeatureSet extends A
  
      /**
       * Creates a new set of features stored in memory. It is caller 
responsibility to ensure that
 -     * <code>{@linkplain Feature#getType()} == type</code> for all elements 
in the given collection
 +     * <code>{@linkplain AbstractFeature#getType()} == type</code> for all 
elements in the given collection
       * (this is not verified).
       *
-      * @param parent     listeners of the parent resource, or {@code null} if 
none.
+      * @param parent     the parent resource, or {@code null} if none.
       * @param type       the type of all features in the given collection.
       * @param features   collection of stored features. This collection will 
not be copied.
       */
-     public MemoryFeatureSet(final StoreListeners parent,
-                             final DefaultFeatureType type, final 
Collection<AbstractFeature> features)
-     {
-         super(parent, false);
 -    public MemoryFeatureSet(final Resource parent, final FeatureType type, 
final Collection<Feature> features) {
++    public MemoryFeatureSet(final Resource parent, final DefaultFeatureType 
type, final Collection<AbstractFeature> features) {
+         super(parent);
          ArgumentChecks.ensureNonNull("type",     type);
          ArgumentChecks.ensureNonNull("features", features);
          this.type     = type;
diff --cc 
storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index b8af67c437,b065ce9b73..a6e50e038f
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@@ -33,7 -34,7 +33,8 @@@ import org.apache.sis.coverage.grid.Gri
  import org.apache.sis.coverage.grid.GridExtent;
  import org.apache.sis.coverage.grid.GridGeometry;
  import org.apache.sis.coverage.grid.GridRoundingMode;
 +import org.apache.sis.coverage.CannotEvaluateException;
+ import org.apache.sis.internal.coverage.RangeArgument;
  import org.apache.sis.storage.AbstractGridCoverageResource;
  import org.apache.sis.storage.DataStoreException;
  import org.apache.sis.storage.RasterLoadingStrategy;
diff --cc 
storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java
index da537fb5a9,cc9f56d6e3..39f5f760d5
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java
@@@ -32,12 -32,12 +32,12 @@@ import org.apache.sis.internal.util.Col
  import org.apache.sis.internal.util.UnmodifiableArrayList;
  import org.apache.sis.internal.storage.Resources;
  import org.apache.sis.storage.AbstractFeatureSet;
- import org.apache.sis.storage.event.StoreListeners;
  import org.apache.sis.storage.Query;
+ import org.apache.sis.storage.Resource;
  
  // Branch-dependent imports
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
  
  
  /**
diff --cc 
storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java
index ef38d87834,898e02089e..61931e80bf
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java
@@@ -200,13 -202,13 +200,13 @@@ public class JoinFeatureSet extends Agg
       * @param  rightAlias   name of the associations to the {@code right} 
features, or {@code null} for a default name.
       * @param  joinType     whether values on both sides are required (inner 
join), or only one side (outer join).
       * @param  condition    join condition as <var>property from left 
feature</var> = <var>property from right feature</var>.
 -     * @param  featureInfo  information about the {@link FeatureType} of this 
feature set.
 +     * @param  featureInfo  information about the {@code FeatureType} of this 
feature set.
       * @throws DataStoreException if an error occurred while creating the 
feature set.
       */
-     public JoinFeatureSet(final StoreListeners parent,
+     public JoinFeatureSet(final Resource parent,
                            final FeatureSet left,  String leftAlias,
                            final FeatureSet right, String rightAlias,
 -                          final Type joinType, final 
BinaryComparisonOperator<? super Feature> condition,
 +                          final Type joinType, final 
BinaryComparisonOperator<? super AbstractFeature> condition,
                            Map<String,?> featureInfo)
              throws DataStoreException
      {

Reply via email to