You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/03/22 18:51:52 UTC
[sis] branch geoapi-4.0 updated (c6a2901fdd -> 01be36ffa4)
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
from c6a2901fdd Merge remote-tracking branch 'origin/feat/filtercopy' into geoapi-4.0. Modifications to the pull request: - Add the missing parameterized types. - Register action for each filter type using the mechanism provided by `Visitor` parent class. - Reuse previously existing filter or expression instances when possible.
new 0e0a37dc5f Minor documentation fixes in comments.
add cbf9f26ab9 Resource : add ResourceProcessor with converted samples operation
add 4f9dc8ee12 chore(Storage): fix a javadoc reference
add aff9e65127 chore(Storage): add a GridCoverageProcessor attribute in resource processor
add 5a6d0d24d0 feat(Storage): add GridCoverageResource resampling capability
add 5a487bb198 chore(Feature): change contract of utility TiledImageMock.initializeAllTiles member function.
add a0a748dea2 feat(Feature): add a computed image for band aggregation
add 17a4412ed7 feat(Feature): allow user to override output color model for band aggregation
add c5b2f14be3 feat(Feature): Add a GridCoverageResource for band aggregation
add 211c9e7af6 feat(Feature+Storage): add a dimension selection grid coverage
add a0722bc11b Merge branch 'geoapi-4.0' into feat/resource-processor
new 01be36ffa4 Merge remote-tracking branch 'origin/feat/resource-processor' into geoapi-4.0. The code in the branch have been reworked for reusing more exising SIS code, for moving some aggregation methods to the dedicated `aggregate` sub-package, for resolving some limitations (e.g. band aggregation no longer requires the same tiling layout), for completing documentation and for retrofitting some operations as `GridCoverageResource.subset(…)`.
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../apache/sis/cloud/aws/s3/CachedByteChannel.java | 2 +-
.../coverage/grid/BandAggregateGridCoverage.java | 312 +++++++
.../sis/coverage/grid/ConvertedGridCoverage.java | 2 +-
.../sis/coverage/grid/DerivedGridCoverage.java | 11 +-
.../apache/sis/coverage/grid/DimensionReducer.java | 5 +-
.../sis/coverage/grid/DimensionalityReduction.java | 986 +++++++++++++++++++++
.../sis/coverage/grid/DisjointExtentException.java | 9 +-
.../sis/coverage/grid/GridCoverageProcessor.java | 194 +++-
.../org/apache/sis/coverage/grid/GridExtent.java | 96 +-
.../sis/coverage/grid/ReducedGridCoverage.java | 160 ++++
.../apache/sis/coverage/grid/SliceGeometry.java | 23 +-
.../org/apache/sis/image/BandAggregateImage.java | 190 ++++
.../java/org/apache/sis/image/BandSelectImage.java | 19 +-
.../org/apache/sis/image/CombinedImageLayout.java | 366 ++++++++
.../java/org/apache/sis/image/ImageProcessor.java | 90 +-
.../main/java/org/apache/sis/image/MaskImage.java | 2 +-
.../java/org/apache/sis/image/PlanarImage.java | 48 +-
.../internal/coverage/MultiSourcesArgument.java | 329 +++++++
.../sis/internal/coverage}/RangeArgument.java | 31 +-
.../internal/coverage/j2d/ColorModelFactory.java | 19 +-
.../sis/internal/coverage/j2d/ImageLayout.java | 22 +-
.../sis/internal/coverage/j2d/ImageUtilities.java | 6 +-
.../apache/sis/internal/coverage/package-info.java | 2 +-
.../org/apache/sis/internal/feature/Resources.java | 35 +
.../sis/internal/feature/Resources.properties | 7 +
.../sis/internal/feature/Resources_fr.properties | 7 +
.../coverage/grid/DimensionalityReductionTest.java | 194 ++++
.../apache/sis/image/BandAggregateImageTest.java | 308 +++++++
.../java/org/apache/sis/image/TiledImageMock.java | 66 +-
.../sis/internal/coverage}/RangeArgumentTest.java | 18 +-
.../apache/sis/test/suite/FeatureTestSuite.java | 3 +
.../referencing/provider/DatumShiftGridLoader.java | 4 +-
.../main/java/org/apache/sis/referencing/CRS.java | 38 +-
.../operation/transform/MathTransforms.java | 35 +
.../operation/transform/PassThroughTransform.java | 124 ++-
.../operation/transform/TransformSeparator.java | 29 +-
.../apache/sis/internal/system/Configuration.java | 2 +-
.../java/org/apache/sis/measure/SystemUnit.java | 2 +-
.../org/apache/sis/measure/UnitFormatTest.java | 2 +-
.../java/org/apache/sis/measure/UnitsTest.java | 2 +-
.../apache/sis/internal/netcdf/RasterResource.java | 2 +-
.../sis/internal/storage/GridResourceWrapper.java | 4 +
.../sis/internal/storage/MemoryGridResource.java | 1 +
.../org/apache/sis/internal/storage/Resources.java | 14 +-
.../sis/internal/storage/Resources.properties | 2 -
.../sis/internal/storage/Resources_fr.properties | 2 -
.../sis/internal/storage/TiledGridResource.java | 1 +
.../sis/internal/storage/esri/AsciiGridStore.java | 2 +-
.../sis/internal/storage/esri/RasterStore.java | 4 +-
.../sis/internal/storage/esri/RawRasterReader.java | 6 +-
.../sis/internal/storage/esri/RawRasterStore.java | 2 +-
.../internal/storage/image/WorldFileResource.java | 2 +-
.../sis/storage/AbstractGridCoverageResource.java | 14 +-
.../org/apache/sis/storage/AbstractResource.java | 20 +-
.../java/org/apache/sis/storage/CoverageQuery.java | 121 ++-
.../org/apache/sis/storage/CoverageSubset.java | 186 ++--
.../apache/sis/storage/GridCoverageResource.java | 6 +-
.../aggregate/BandAggregateGridResource.java | 379 ++++++++
.../aggregate/ConcatenatedGridResource.java | 70 +-
.../internal/storage/MemoryGridResourceTest.java | 2 -
.../org/apache/sis/storage/CoverageSubsetTest.java | 130 +++
.../aggregate/BandAggregateGridResourceTest.java | 161 ++++
.../apache/sis/test/suite/StorageTestSuite.java | 3 +-
63 files changed, 4629 insertions(+), 305 deletions(-)
create mode 100644 core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
create mode 100644 core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
create mode 100644 core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ReducedGridCoverage.java
create mode 100644 core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
create mode 100644 core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourcesArgument.java
rename {storage/sis-storage/src/main/java/org/apache/sis/internal/storage => core/sis-feature/src/main/java/org/apache/sis/internal/coverage}/RangeArgument.java (94%)
create mode 100644 core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
create mode 100644 core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
rename {storage/sis-storage/src/test/java/org/apache/sis/internal/storage => core/sis-feature/src/test/java/org/apache/sis/internal/coverage}/RangeArgumentTest.java (83%)
create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
create mode 100644 storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageSubsetTest.java
create mode 100644 storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
[sis] 02/02: Merge remote-tracking branch 'origin/feat/resource-processor' into geoapi-4.0. The code in the branch have been reworked for reusing more exising SIS code, for moving some aggregation methods to the dedicated `aggregate` sub-package, for resolving some limitations (e.g. band aggregation no longer requires the same tiling layout), for completing documentation and for retrofitting some operations as `GridCoverageResource.subset(…)`.
Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 01be36ffa4f56ec49d6acf255e9e3d56c9097395
Merge: 0e0a37dc5f a0722bc11b
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Mar 22 19:47:55 2023 +0100
Merge remote-tracking branch 'origin/feat/resource-processor' into geoapi-4.0.
The code in the branch have been reworked for reusing more exising SIS code,
for moving some aggregation methods to the dedicated `aggregate` sub-package,
for resolving some limitations (e.g. band aggregation no longer requires the
same tiling layout), for completing documentation and for retrofitting some
operations as `GridCoverageResource.subset(…)`.
.../coverage/grid/BandAggregateGridCoverage.java | 312 +++++++
.../sis/coverage/grid/ConvertedGridCoverage.java | 2 +-
.../sis/coverage/grid/DerivedGridCoverage.java | 11 +-
.../apache/sis/coverage/grid/DimensionReducer.java | 5 +-
.../sis/coverage/grid/DimensionalityReduction.java | 986 +++++++++++++++++++++
.../sis/coverage/grid/DisjointExtentException.java | 9 +-
.../sis/coverage/grid/GridCoverageProcessor.java | 194 +++-
.../org/apache/sis/coverage/grid/GridExtent.java | 96 +-
.../sis/coverage/grid/ReducedGridCoverage.java | 160 ++++
.../apache/sis/coverage/grid/SliceGeometry.java | 23 +-
.../org/apache/sis/image/BandAggregateImage.java | 190 ++++
.../java/org/apache/sis/image/BandSelectImage.java | 19 +-
.../org/apache/sis/image/CombinedImageLayout.java | 366 ++++++++
.../java/org/apache/sis/image/ImageProcessor.java | 90 +-
.../main/java/org/apache/sis/image/MaskImage.java | 2 +-
.../java/org/apache/sis/image/PlanarImage.java | 48 +-
.../internal/coverage/MultiSourcesArgument.java | 329 +++++++
.../sis/internal/coverage}/RangeArgument.java | 31 +-
.../internal/coverage/j2d/ColorModelFactory.java | 19 +-
.../sis/internal/coverage/j2d/ImageLayout.java | 22 +-
.../sis/internal/coverage/j2d/ImageUtilities.java | 6 +-
.../apache/sis/internal/coverage/package-info.java | 2 +-
.../org/apache/sis/internal/feature/Resources.java | 35 +
.../sis/internal/feature/Resources.properties | 7 +
.../sis/internal/feature/Resources_fr.properties | 7 +
.../coverage/grid/DimensionalityReductionTest.java | 194 ++++
.../apache/sis/image/BandAggregateImageTest.java | 308 +++++++
.../java/org/apache/sis/image/TiledImageMock.java | 66 +-
.../sis/internal/coverage}/RangeArgumentTest.java | 18 +-
.../apache/sis/test/suite/FeatureTestSuite.java | 3 +
.../main/java/org/apache/sis/referencing/CRS.java | 38 +-
.../operation/transform/MathTransforms.java | 35 +
.../operation/transform/PassThroughTransform.java | 124 ++-
.../operation/transform/TransformSeparator.java | 29 +-
.../apache/sis/internal/netcdf/RasterResource.java | 2 +-
.../sis/internal/storage/GridResourceWrapper.java | 4 +
.../sis/internal/storage/MemoryGridResource.java | 1 +
.../org/apache/sis/internal/storage/Resources.java | 14 +-
.../sis/internal/storage/Resources.properties | 2 -
.../sis/internal/storage/Resources_fr.properties | 2 -
.../sis/internal/storage/TiledGridResource.java | 1 +
.../sis/internal/storage/esri/AsciiGridStore.java | 2 +-
.../sis/internal/storage/esri/RasterStore.java | 4 +-
.../sis/internal/storage/esri/RawRasterReader.java | 6 +-
.../sis/internal/storage/esri/RawRasterStore.java | 2 +-
.../internal/storage/image/WorldFileResource.java | 2 +-
.../sis/storage/AbstractGridCoverageResource.java | 14 +-
.../org/apache/sis/storage/AbstractResource.java | 20 +-
.../java/org/apache/sis/storage/CoverageQuery.java | 121 ++-
.../org/apache/sis/storage/CoverageSubset.java | 186 ++--
.../apache/sis/storage/GridCoverageResource.java | 6 +-
.../aggregate/BandAggregateGridResource.java | 379 ++++++++
.../aggregate/ConcatenatedGridResource.java | 70 +-
.../internal/storage/MemoryGridResourceTest.java | 2 -
.../org/apache/sis/storage/CoverageSubsetTest.java | 130 +++
.../aggregate/BandAggregateGridResourceTest.java | 161 ++++
.../apache/sis/test/suite/StorageTestSuite.java | 3 +-
57 files changed, 4622 insertions(+), 298 deletions(-)
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
index 0000000000,0000000000..c84fd1e517
new file mode 100644
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
@@@ -1,0 -1,0 +1,312 @@@
++/*
++ * 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.TreeMap;
++import java.awt.image.ColorModel;
++import java.awt.image.RenderedImage;
++import org.opengis.geometry.DirectPosition;
++import org.opengis.referencing.operation.TransformException;
++import org.apache.sis.image.DataType;
++import org.apache.sis.image.ImageProcessor;
++import org.apache.sis.internal.feature.Resources;
++import org.apache.sis.internal.coverage.MultiSourcesArgument;
++import org.apache.sis.internal.util.CollectionsExt;
++
++
++/**
++ * A grid coverage where each band (sample dimension) is taken from a selection of bands
++ * in a sequence of source coverages.
++ *
++ * <h2>Restrictions</h2>
++ * <ul>
++ * <li>All coverages shall have the same {@linkplain GridCoverage#getGridGeometry() domain}, except for
++ * the grid extent and the translation terms which can vary by integer amounts of grid cells.</li>
++ * <li>All grid extents shall intersect and the intersection area shall be non-empty.</li>
++ * <li>If coverage data are stored in {@link RenderedImage} instances,
++ * then all images shall use the same data type.</li>
++ * </ul>
++ *
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ * @since 1.4
++ */
++final class BandAggregateGridCoverage extends GridCoverage {
++ /**
++ * The source grid coverages.
++ */
++ private final GridCoverage[] sources;
++
++ /**
++ * The sample dimensions to use for each source coverage, in order.
++ * The length of this array is always equal to {@link #sources} array length.
++ * This array may contain {@code null} elements but never contain empty element.
++ */
++ private final int[][] bandsPerSource;
++
++ /**
++ * Total number of bands.
++ */
++ private final int numBands;
++
++ /**
++ * Index of a sources having the same "grid to CRS" than this grid coverage, or -1 if none.
++ */
++ private final int sourceOfGridToCRS;
++
++ /**
++ * The data type identifying the primitive type used for storing sample values in each band.
++ */
++ private final DataType dataType;
++
++ /**
++ * The color model to apply on aggregated image, or {@code null} for default.
++ * If {@code null}, the color model will be inferred from the aggregated number
++ * of bands and the sample data type.
++ */
++ private final ColorModel colors;
++
++ /**
++ * The processor to use for creating images.
++ */
++ private final ImageProcessor processor;
++
++ /**
++ * Creates a new band aggregated coverage from the given sources.
++ *
++ * @param aggregate the source grid coverages together with bands to select.
++ * @param colors the color model to apply on aggregated image, or {@code null} for default.
++ * @param processor the processor to use for creating images.
++ * @throws IllegalArgumentException if there is an incompatibility between some source coverages
++ * or if some band indices are duplicated or outside their range of validity.
++ */
++ BandAggregateGridCoverage(final MultiSourcesArgument<GridCoverage> aggregate, final ColorModel colors,
++ final ImageProcessor processor)
++ {
++ super(aggregate.domain(GridCoverage::getGridGeometry), aggregate.ranges());
++ this.sources = aggregate.sources();
++ this.bandsPerSource = aggregate.bandsPerSource();
++ this.numBands = aggregate.numBands();
++ this.sourceOfGridToCRS = aggregate.sourceOfGridToCRS();
++ this.colors = colors;
++ this.processor = processor;
++ this.dataType = sources[0].getBandType();
++ for (int i=1; i < sources.length; i++) {
++ final GridCoverage source = sources[i];
++ final DataType type = source.getBandType();
++ if (!dataType.equals(type)) {
++ throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedDataType));
++ }
++ }
++ }
++
++ /**
++ * Returns the data type identifying the primitive type used for storing sample values in each band.
++ */
++ @Override
++ DataType getBandType() {
++ return dataType;
++ }
++
++ /**
++ * Returns a two-dimensional slice of grid data as a rendered image.
++ * This operation is potentially costly if the {@code sliceExtent} argument changes often because
++ * the previously computed images are unlikely to be reused if the coordinate systems are different.
++ * It may result in the same bands being copied may times in different {@link RenderedImage} instances.
++ *
++ * <h4>Implementation note</h4>
++ * We do not compute the rendered image in advance (which would have produced better caching) because
++ * the image to cache depends on {@code sliceExtent} if this coverage has more than two dimensions.
++ *
++ * @param sliceExtent a subspace of this grid coverage extent where all dimensions except two have a size of 1 cell.
++ * @return the grid slice as a rendered image. Image location is relative to {@code sliceExtent}.
++ */
++ @Override
++ public RenderedImage render(GridExtent sliceExtent) {
++ /*
++ * We need a non-null extent for making sure that all rendered images
++ * will use coordinates relative to the same extent.
++ */
++ if (sliceExtent == null) {
++ sliceExtent = gridGeometry.getExtent();
++ }
++ final RenderedImage[] images = new RenderedImage[sources.length];
++ for (int i=0; i<images.length; i++) {
++ images[i] = sources[i].render(sliceExtent);
++ }
++ return processor.aggregateBands(images, bandsPerSource, colors);
++ }
++
++ /**
++ * Creates a new function for computing or interpolating sample values at given locations.
++ */
++ @Override
++ public Evaluator evaluator() {
++ return new CombinedEvaluator(sources);
++ }
++
++ /**
++ * An evaluator which delegates to the evaluators of all source grid coverages.
++ * This class is not thread-safe.
++ *
++ * <h2>Implementation note</h2>
++ * We need this specialized class instead of relying on the default implementation inherited
++ * from {@link GridCoverage} because the latter may invoke {@link #render(GridExtent)} often,
++ * which is potentially costly. Furthermore, it may not work well with coverages having more
++ * than two dimensions.
++ */
++ private final class CombinedEvaluator implements Evaluator {
++ /**
++ * Evaluators from all source coverages.
++ */
++ private final Evaluator[] sources;
++
++ /**
++ * The union of slice coordinates from all sources, or {@code null} if not yet computed.
++ * All coverages should have the same values, but we nevertheless compute union in case.
++ */
++ private Map<Integer,Long> slices;
++
++ /**
++ * Creates a new evaluator which will delegate to the evaluators of all given sources.
++ */
++ CombinedEvaluator(final GridCoverage[] coverages) {
++ sources = new Evaluator[coverages.length];
++ for (int i=0; i < coverages.length; i++) {
++ sources[i] = coverages[i].evaluator();
++ }
++ }
++
++ /**
++ * Returns the coverage that created this evaluator.
++ */
++ @Override
++ public GridCoverage getCoverage() {
++ return BandAggregateGridCoverage.this;
++ }
++
++ /**
++ * Returns the default slice where to perform evaluation, or an empty map if unspecified.
++ */
++ @Override
++ @SuppressWarnings("ReturnOfCollectionOrArrayField")
++ public Map<Integer,Long> getDefaultSlice() {
++ if (slices == null) {
++ final var c = new TreeMap<Integer,Long>();
++ for (final Evaluator source : sources) {
++ c.putAll(source.getDefaultSlice());
++ }
++ slices = CollectionsExt.unmodifiableOrCopy(c);
++ }
++ return slices;
++ }
++
++ /**
++ * Sets the default slice where to perform evaluation, or an empty map if unspecified.
++ */
++ @Override
++ public void setDefaultSlice(final Map<Integer, Long> slice) {
++ slices = null;
++ for (final Evaluator source : sources) {
++ source.setDefaultSlice(slice);
++ }
++ }
++
++ /**
++ * Returns whether to return {@code null} instead of throwing an exception if a point is outside coverage bounds.
++ */
++ @Override
++ public boolean isNullIfOutside() {
++ for (final Evaluator source : sources) {
++ if (!source.isNullIfOutside()) {
++ return false;
++ }
++ }
++ return true;
++ }
++
++ /**
++ * Sets whether to return {@code null} instead of throwing an exception if a point is outside coverage bounds.
++ */
++ @Override
++ public void setNullIfOutside(final boolean flag) {
++ for (final Evaluator source : sources) {
++ source.setNullIfOutside(flag);
++ }
++ }
++
++ /**
++ * Returns {@code true} if this evaluator is allowed to wraparound coordinates that are outside the coverage.
++ */
++ @Override
++ public boolean isWraparoundEnabled() {
++ for (final Evaluator source : sources) {
++ if (!source.isWraparoundEnabled()) {
++ return false;
++ }
++ }
++ return true;
++ }
++
++ /**
++ * Sets whether this evaluator is allowed to wraparound coordinates that are outside the coverage.
++ */
++ @Override
++ public void setWraparoundEnabled(final boolean allow) {
++ for (final Evaluator source : sources) {
++ source.setWraparoundEnabled(allow);
++ }
++ }
++
++ /**
++ * Returns a sequence of double values for a given point in the coverage.
++ * This method delegates to all source evaluators and merge the results.
++ */
++ @Override
++ public double[] apply(final DirectPosition point) {
++ final double[] aggregate = new double[numBands];
++ int offset = 0;
++ for (int i=0; i < sources.length; i++) {
++ final double[] values = sources[i].apply(point);
++ final int[] bands = bandsPerSource[i];
++ if (bands == null) {
++ System.arraycopy(values, 0, aggregate, offset, values.length);
++ offset += values.length;
++ } else {
++ for (int b : bands) {
++ values[offset++] = values[b];
++ }
++ }
++ }
++ return aggregate;
++ }
++
++ /**
++ * Converts the specified geospatial position to grid coordinates.
++ */
++ @Override
++ public FractionalGridCoordinates toGridCoordinates(DirectPosition point) throws TransformException {
++ if (sourceOfGridToCRS >= 0) {
++ return sources[sourceOfGridToCRS].toGridCoordinates(point);
++ } else {
++ throw new UnsupportedOperationException();
++ }
++ }
++ }
++}
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
index 5a52098d67,5a52098d67..e2c4a0ce3d
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
@@@ -224,7 -224,7 +224,7 @@@ final class ConvertedGridCoverage exten
* @see GridCoverageProcessor.Optimization#REPLACE_SOURCE
*/
@Override
-- final boolean IsNotRepleacable() {
++ final boolean isNotRepleacable() {
return isUSerDefined;
}
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java
index ca8e0b5d18,ca8e0b5d18..628b0dfca1
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java
@@@ -29,10 -29,10 +29,10 @@@ import org.apache.sis.util.Debug
/**
-- * A grid coverage which is derived from a single source coverage,
-- * The default implementations of methods in this class assume that this derived coverage
-- * uses the same sample dimensions than the source coverage. If it is not the case, then
-- * some methods may need to be overridden.
++ * A grid coverage which is derived from a single source coverage.
++ * The default implementations of methods in this class assume that this
++ * derived coverage uses the same sample dimensions than the source coverage.
++ * If it is not the case, then some methods may need to be overridden.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.3
@@@ -75,7 -75,7 +75,7 @@@ abstract class DerivedGridCoverage exte
*
* @see GridCoverageProcessor.Optimization#REPLACE_SOURCE
*/
-- boolean IsNotRepleacable() {
++ boolean isNotRepleacable() {
return false;
}
@@@ -112,6 -112,6 +112,7 @@@
* @return a tree representation of the specified elements.
*/
@Debug
++ @Override
public TreeTable toTree(final Locale locale, final int bitmask) {
final TreeTable tree = super.toTree(locale, bitmask);
final TreeTable.Node branch = tree.getRoot().newChild();
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionReducer.java
index b494c6e510,b494c6e510..2fa967ca18
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionReducer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionReducer.java
@@@ -33,6 -33,6 +33,9 @@@ import org.apache.sis.referencing.CRS
* This is used when the Area Of Interest has more dimensions than the grid geometry, in which case the
* transform from grid to AOI will fail if we do not discard the extra dimensions.
*
++ * <p>This class works on the CRS dimensions.
++ * This is different than {@link DimensionalityReduction}, which works on grid dimensions.</p>
++ *
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
@@@ -49,7 -49,7 +52,7 @@@ final class DimensionReducer
private CoordinateReferenceSystem reducedCRS;
/**
-- * Creates an helper which will retain only the {@code targetCRS} dimensions that are found the the base grid.
++ * Creates an helper which will retain only the {@code targetCRS} dimensions that are found in the base grid.
* This will be used by caller for creating a valid {@code sourceCRS} to {@code targetCRS} transform.
*
* @param base the grid geometry which will be derived. Cannot be null.
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
index 0000000000,0000000000..14e876c2f7
new file mode 100644
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
@@@ -1,0 -1,0 +1,986 @@@
++/*
++ * 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;
++
++
++/**
++ * 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 final class DimensionalityReduction implements UnaryOperator<GridCoverage>, Serializable {
++ /**
++ * For cross-version compatibility.
++ */
++ private static final long serialVersionUID = -6462887684250336261L;
++
++ /**
++ * The source grid geometry with all dimensions.
++ */
++ 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;
++ }
++
++ /**
++ * Creates information about reducing the number of dimensions of the specified grid 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.
++ */
++ private 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 can not 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 equals, 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/DisjointExtentException.java
index 41959dea3e,41959dea3e..815eb43b17
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DisjointExtentException.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DisjointExtentException.java
@@@ -20,8 -20,8 +20,13 @@@ import org.apache.sis.internal.feature.
/**
-- * Thrown when operations on a {@link GridGeometry} result in an area which
-- * does not intersect anymore the {@link GridExtent} of the {@link GridGeometry}.
++ * Thrown when the domain of two grid coverages or images do not intersect.
++ * The domain of a {@link GridCoverage} is its {@link GridGeometry}.
++ * The domain of a {@link java.awt.image.RenderedImage} is its range of pixel coordinates,
++ * which can be part of a grid geometry definition.
++ *
++ * <p>This exception may also be thrown when operations on a {@link GridGeometry} result in an area
++ * which does not intersect anymore the {@link GridExtent} of the original {@link GridGeometry}.</p>
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index c279ebefa1,23079340c5..b59baccab4
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@@ -29,13 -29,15 +29,16 @@@ import javax.measure.Quantity
import org.opengis.util.FactoryException;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
++import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.coverage.RegionOfInterest;
import org.apache.sis.coverage.SampleDimension;
++import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.image.DataType;
import org.apache.sis.image.ImageProcessor;
import org.apache.sis.image.Interpolation;
++import org.apache.sis.internal.coverage.MultiSourcesArgument;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.collection.WeakHashSet;
@@@ -51,6 -53,6 +54,7 @@@ import org.apache.sis.measure.NumberRan
* {@code GridCoverageProcessor} is safe for concurrent use in multi-threading environment.
*
* @author Martin Desruisseaux (Geomatys)
++ * @author Alexis Manin (Geomatys)
* @version 1.4
*
* @see org.apache.sis.image.ImageProcessor
@@@ -446,7 -448,7 +450,7 @@@ public class GridCoverageProcessor impl
return source;
} else if (allowSourceReplacement && source instanceof DerivedGridCoverage) {
final DerivedGridCoverage derived = (DerivedGridCoverage) source;
-- if (derived.IsNotRepleacable()) break;
++ if (derived.isNotRepleacable()) break;
source = derived.source;
} else {
break;
@@@ -500,6 -502,53 +504,180 @@@
return resample(source, new GridGeometry(null, PixelInCell.CELL_CENTER, null, target));
}
+ /**
- * Remove all "flat" grid dimensions from input data. Flat dimensions are dimensions with a single grid cell.
- * @param source The coverage we want to reduce to lower dimension.
- * @return Either input coverage if no dimension can be removed, or a view of the coverage with less dimensions.
++ * Automatically reduces a grid coverage dimensionality by removing all grid axes with an extent size of 1.
++ * Axes in the reduced grid coverage will be in the same order than in the source coverage.
++ *
++ * @param source the coverage to reduce to a lower number of dimensions.
++ * @return the reduced grid coverage, or {@code source} if no grid dimensions can be removed.
++ *
++ * @see DimensionalityReduction#reduce(GridGeometry)
++ *
++ * @since 1.4
+ */
- public GridCoverage squeeze(GridCoverage source) {
- return GridDimensionSelection.squeeze(source.getGridGeometry())
- .<GridCoverage>map(spec -> new DimensionSelectionCoverage(source, spec))
- .orElse(source);
++ public GridCoverage reduceDimensionality(final GridCoverage source) {
++ return DimensionalityReduction.reduce(source.getGridGeometry()).apply(source);
+ }
+
+ /**
- * Create a coverage containing only specified dimensions.
- *
- * Constraints:
- * <ul>
- * <li>Removed dimensions must have only one degree of liberty.</li>
- * <li>Output dimension order is the same as in source coverage, whatever order axes are given as input.</li>
- * <li>If input dataset contains dimensions that are not separable, but only part of them are selected, this code will fail.</li>
- * </ul>
- *
- * @param source The coverage to reduce to lower dimension.
- * @param gridAxesToPreserve Index of each grid dimension to maintain in result. Must contain at least one element.
++ * Creates a coverage trimmed from the specified grid dimensions.
++ * This is a <cite>dimensionality reduction</cite> operation applied to the coverage domain.
++ * The dimensions to remove are specified as indices of <em>grid extent</em> axes.
++ * It may be the same indices than the indices of the CRS axes which will be removed,
++ * but not necessarily.
++ *
++ * <h4>Constraints</h4>
++ * If the source coverage contains dimensions that are not
++ * {@linkplain org.apache.sis.referencing.operation.transform.TransformSeparator separable}
++ * and if only a subset of those dimensions are specified for removal,
++ * then this method will throw an {@link IllegalGridGeometryException}.
++ *
++ * <p>For each dimension that is removed,
++ * the {@linkplain GridExtent#getSize(int) size} of the grid extent must be 1 cell.
++ * If this condition does not hold, then this method will throw a {@link SubspaceNotSpecifiedException}.
++ * If desired, this restriction can be relaxed by direct use of {@link DimensionalityReduction} as below,
++ * where (<var>x</var>, <var>y</var>, <var>z</var>, <var>t</var>) are grid coordinates of a point
++ * in the desired slice:</p>
++ *
++ * {@snippet lang="java" :
++ * var reduction = DimensionalityReduction.remove(source.getGridGeometry(), gridAxesToPass);
++ * reduction = reduction.withSlicePoint(x, y, z, t);
++ * GridCoverage output = reduction.apply(source);
++ * }
++ *
++ * Alternatively the {@code withSlicePoint(…)} call can be omitted if the caller knows that the source
++ * coverage can handle {@linkplain DimensionalityReduction#reverse(GridExtent) ambiguous grid extents}.
++ *
++ * @param source the coverage to reduce to a lower number of dimensions.
++ * @param gridAxesToRemove indices of each grid dimension to strip from result. Duplicated values are ignored.
++ * @return the reduced grid coverage, or {@code source} if no grid dimensions was specified.
++ * @throws IndexOutOfBoundsException if a grid axis index is out of bounds.
++ * @throws SubspaceNotSpecifiedException if at least one removed dimension has a grid extent size larger than 1 cell.
++ * @throws IllegalGridGeometryException if the dimensions to kept cannot be separated from the dimensions to omit.
++ *
++ * @see DimensionalityReduction#remove(GridGeometry, int...)
++ *
++ * @since 1.4
++ */
++ public GridCoverage removeGridDimensions(final GridCoverage source, final int... gridAxesToRemove) {
++ var reduction = DimensionalityReduction.remove(source.getGridGeometry(), gridAxesToRemove);
++ reduction.ensureIsSlice();
++ return reduction.apply(source);
++ }
++
++ /**
++ * Creates a coverage containing only the specified grid dimensions.
++ * This is a <cite>dimensionality reduction</cite> operation applied to the coverage domain.
++ * The dimensions to keep are specified as indices of <em>grid extent</em> axes.
++ * It may be the same indices than the indices of the CRS axes which will pass through,
++ * but not necessarily.
++ *
++ * <p>The axis order in the returned coverage is always the same as in the given {@code source} coverage,
++ * whatever the order in which axes are specified as input in the {@code gridAxesToPass} array.
++ * Duplicated values in the array are also ignored.</p>
++ *
++ * <h4>Constraints</h4>
++ * If the source coverage contains dimensions that are not
++ * {@linkplain org.apache.sis.referencing.operation.transform.TransformSeparator separable}
++ * and if only a subset of those dimensions are selected in the {@code gridAxesToPass} array,
++ * then this method will throw an {@link IllegalGridGeometryException}.
++ *
++ * <p>For each dimension that is not passed to the output grid coverage,
++ * the {@linkplain GridExtent#getSize(int) size} of the grid extent must be 1 cell.
++ * If this condition does not hold, then this method will throw a {@link SubspaceNotSpecifiedException}.
++ * If desired, this restriction can be relaxed by direct use of {@link DimensionalityReduction} as below,
++ * where (<var>x</var>, <var>y</var>, <var>z</var>, <var>t</var>) are grid coordinates of a point
++ * in the desired slice:</p>
++ *
++ * {@snippet lang="java" :
++ * var reduction = DimensionalityReduction.select(source.getGridGeometry(), gridAxesToPass);
++ * reduction = reduction.withSlicePoint(x, y, z, t);
++ * GridCoverage output = reduction.apply(source);
++ * }
++ *
++ * Alternatively the {@code withSlicePoint(…)} call can be omitted if the caller knows that the source
++ * coverage can handle {@linkplain DimensionalityReduction#reverse(GridExtent) ambiguous grid extents}.
++ *
++ * @param source the coverage to reduce to a lower number of dimensions.
++ * @param gridAxesToPass indices of each grid dimension to maintain in result. Order and duplicated values are ignored.
++ * @return the reduced grid coverage, or {@code source} if all grid dimensions where specified.
++ * @throws IndexOutOfBoundsException if a grid axis index is out of bounds.
++ * @throws SubspaceNotSpecifiedException if at least one removed dimension has a grid extent size larger than 1 cell.
++ * @throws IllegalGridGeometryException if the dimensions to kept cannot be separated from the dimensions to omit.
++ *
++ * @see DimensionalityReduction#select(GridGeometry, int...)
++ *
++ * @since 1.4
+ */
- public GridCoverage selectDimensions(GridCoverage source, int... gridAxesToPreserve) {
- final GridDimensionSelection.Specification spec = GridDimensionSelection.preserve(source.getGridGeometry(), gridAxesToPreserve);
- return new DimensionSelectionCoverage(source, spec);
++ public GridCoverage selectGridDimensions(final GridCoverage source, final int... gridAxesToPass) {
++ var reduction = DimensionalityReduction.select(source.getGridGeometry(), gridAxesToPass);
++ reduction.ensureIsSlice();
++ return reduction.apply(source);
+ }
+
+ /**
- * Create a coverage trimmed from specified <em>grid</em> dimensions.
++ * Aggregates in a single coverage the ranges of all specified coverages, in order.
++ * The {@linkplain GridCoverage#getSampleDimensions() list of sample dimensions} of
++ * the aggregated coverage will be the concatenation of the lists of all sources.
++ *
++ * <h4>Restrictions</h4>
++ * All coverages shall have compatible domain, defined as below:
+ *
- * Constraints:
+ * <ul>
- * <li>Removed dimensions must have only one degree of liberty.</li>
- * <li>Output dimension order is the same as in source coverage.</li>
- * <li>If input dataset contains dimensions that are not separable, but only part of them are selected for removal, this code will fail.</li>
++ * <li>Same CRS.</li>
++ * <li>Same <cite>grid to CRS</cite> transform except for translation terms.</li>
++ * <li>Translation terms that differ only by an integer amount of grid cells.</li>
+ * </ul>
+ *
- * @param source Dataset to reduce.
- * @param gridAxesToRemove Index of each grid dimension to strip from result. Must contain at least one element.
++ * The intersection of the domain of all coverages shall be non-empty,
++ * and all coverages shall use the same data type in their rendered image.
++ *
++ * @param sources coverages whose ranges shall be aggregated, in order. At least one coverage must be provided.
++ * @return the aggregated coverage, or {@code sources[0]} returned directly if only one coverage was supplied.
++ * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
++ *
++ * @see #aggregateRanges(GridCoverage[], int[][], ColorModel)
++ * @see ImageProcessor#aggregateBands(RenderedImage...)
++ *
++ * @since 1.4
++ */
++ public GridCoverage aggregateRanges(final GridCoverage... sources) {
++ return aggregateRanges(sources, null, null);
++ }
++
++ /**
++ * Aggregates in a single coverage the specified bands of a sequence of source coverages, in order.
++ * This method performs the same work than {@link #aggregateRanges(GridCoverage...)},
++ * but with the possibility to specify the bands to retain in each source coverage.
++ * The {@code bandsPerSource} argument specifies the bands to select in each source coverage.
++ * That array can be {@code null} for selecting all bands in all source coverages,
++ * or may contain {@code null} elements for selecting all bands of the corresponding coverage.
++ * An empty array element (i.e. zero band to select) discards the corresponding source coverage.
++ * In the latter case, the discarded element in the {@code sources} array may be {@code null}.
++ *
++ * @param sources coverages whose bands shall be aggregated, in order. At least one coverage must be provided.
++ * @param bandsPerSource bands to use for each source coverage, in order.
++ * May be {@code null} or may contain {@code null} elements.
++ * @param colors the color model to apply on aggregated image, or {@code null} for inferring
++ * a default color model using aggregated number of bands and sample data type.
++ * @return the aggregated coverage, or one of the sources if it can be used directly.
++ * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
++ * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
++ *
++ * @see ImageProcessor#aggregateBands(RenderedImage[], int[][], ColorModel)
++ *
++ * @since 1.4
+ */
- public GridCoverage removeDimensions(GridCoverage source, int... gridAxesToRemove) {
- final GridDimensionSelection.Specification spec = GridDimensionSelection.remove(source.getGridGeometry(), gridAxesToRemove);
- return new DimensionSelectionCoverage(source, spec);
++ public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] bandsPerSource, ColorModel colors) {
++ final var aggregate = new MultiSourcesArgument<>(sources, bandsPerSource);
++ aggregate.identityAsNull();
++ aggregate.validate(GridCoverage::getSampleDimensions);
++ if (aggregate.isIdentity()) {
++ return aggregate.sources()[0];
++ }
++ return new BandAggregateGridCoverage(aggregate, colors, imageProcessor);
+ }
+
/**
* Invoked when an ignorable exception occurred.
*
@@@ -521,7 -570,7 +699,15 @@@
public boolean equals(final Object object) {
if (object != null && object.getClass() == getClass()) {
final GridCoverageProcessor other = (GridCoverageProcessor) object;
-- return imageProcessor.equals(other.imageProcessor);
++ if (imageProcessor.equals(other.imageProcessor)) {
++ final EnumSet<?> optimizations;
++ synchronized (this) {
++ optimizations = (EnumSet<?>) this.optimizations.clone();
++ }
++ synchronized (other) {
++ return optimizations.equals(other.optimizations);
++ }
++ }
}
return false;
}
@@@ -532,8 -581,8 +718,8 @@@
* @return a hash code value for this processor.
*/
@Override
-- public int hashCode() {
-- return Objects.hash(getClass(), imageProcessor);
++ public synchronized int hashCode() {
++ return Objects.hash(getClass(), imageProcessor, optimizations);
}
/**
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index a023268b90,a023268b90..176adfe28a
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@@ -141,6 -141,6 +141,7 @@@ public class GridExtent implements Grid
* The same array may be shared by many {@code GridExtent} instances.
*
* @see #getAxisType(int)
++ * @see #getAxisTypes()
*/
private final DimensionNameType[] types;
@@@ -148,6 -148,6 +149,10 @@@
* Minimum and maximum grid coordinates. The first half contains minimum coordinates (inclusive),
* while the last half contains maximum coordinates (<strong>inclusive</strong>). Note that the
* later inclusiveness is the opposite of Java2D usage but conforms to ISO specification.
++ *
++ * @see #getLow(int)
++ * @see #getHigh(int)
++ * @see #getCoordinates()
*/
private final long[] coordinates;
@@@ -783,6 -783,6 +788,33 @@@
return (low >> 1) + (high >> 1) + ((low | high) & 1);
}
++ /**
++ * Returns a grid coordinate at the given relative position between <var>low</var> and <var>high</var>.
++ * The relative position is specified by a ratio between 0 and 1 where
++ * 0 maps to {@linkplain #getLow(int) low} grid coordinates,
++ * 1 maps to {@linkplain #getHigh(int) high grid coordinates} and
++ * 0.5 maps to {@linkplain #getMedian(int) median grid coordinates}.
++ * Ratio values outside the [0 … 1] range result in extrapolations.
++ *
++ * @param index the dimension for which to obtain the interpolated coordinate.
++ * @param ratio interpolation ratio (0 for low, 0.5 for median, 1 for high coordinate).
++ * @return the interpolated coordinate value in the given dimension.
++ * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
++ * than the {@linkplain #getDimension() grid dimension}.
++ * @throws ArithmeticException if the extrapolated coordinate cannot be represented as a 64 bits integer.
++ *
++ * @since 1.4
++ */
++ public long getRelative(final int index, final double ratio) {
++ if (ratio == 0.5) {
++ return getMedian(index); // Special case for avoiding rounding errors.
++ } else if (ratio == 1) {
++ return getHigh(index); // Special case for avoiding rounding errors.
++ } else {
++ return Math.addExact(getLow(index), Math.round(getSize(index, true) * ratio));
++ }
++ }
++
/**
* Returns the number of integer grid coordinates along the specified dimension.
* This is equal to {@code getHigh(dimension) - getLow(dimension) + 1}.
@@@ -824,6 -824,6 +856,17 @@@
return Numerics.toUnsignedDouble(size);
}
++ /**
++ * Returns the low and high coordinates packaged in a single array.
++ * The length of this array is twice the number of dimensions.
++ * Changes in the returned array do not modify this extent.
++ *
++ * @return an array with low coordinates first, followed by high coordinates.
++ */
++ final long[] getCoordinates() {
++ return coordinates.clone();
++ }
++
/**
* Returns the grid coordinates of a representative point.
* This point may be used for estimating a {@linkplain GridGeometry#getResolution(boolean) grid resolution}.
@@@ -905,10 -905,10 +948,10 @@@
*
* {@snippet lang="java" :
* int[] subDimensions = slice4D.getExtent().getSubspaceDimensions(2);
-- * GridGeometry slice2D = slice4D.reduce(subDimensions);
++ * GridGeometry slice2D = slice4D.selectDimensions(subDimensions);
* }
*
-- * Note that in this particular example, it would have been more efficient to execute {@code grid.reduce(1,2)} directly.
++ * Note that in this example, it would have been more efficient to execute {@code grid.selectDimensions(1,2)} directly.
* This {@code getSubspaceDimensions(int)} method is more useful for inferring a {@code slice2D} from a {@code slice4D}
* which has been created elsewhere, or when we do not really want the {@code slice2D} but only its dimension indices.
*
@@@ -1280,6 -1280,6 +1323,7 @@@
*
* @see #getSubspaceDimensions(int)
* @see GridGeometry#selectDimensions(int...)
++ * @see DimensionalityReduction#apply(GridExtent)
*
* @since 1.3
*/
@@@ -1555,10 -1555,10 +1599,12 @@@
* @param slicePoint a pre-allocated direct position to be overwritten by this method.
* @param sliceRatio the ratio to apply on all grid dimensions except the ones to keep.
* @param dimensionsToKeep the grid dimension to keep unchanged.
++ *
++ * @see #getRelative(int, double)
*/
final GridExtent sliceByRatio(final DirectPosition slicePoint, final double sliceRatio, final int[] dimensionsToKeep) {
for (int i=slicePoint.getDimension(); --i >= 0;) {
-- slicePoint.setOrdinate(i, sliceRatio * getSize(i, true) + getLow(i)); // TODO: use Math.fma
++ slicePoint.setOrdinate(i, Math.fma(sliceRatio, getSize(i, true), getLow(i)));
}
for (int i=0; i<dimensionsToKeep.length; i++) {
slicePoint.setOrdinate(dimensionsToKeep[i], Double.NaN);
@@@ -1762,24 -1762,24 +1808,9 @@@
* @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension.
*/
private GridExtent combine(final GridExtent other, final boolean union) {
++ ensureSameAxes(other, "other");
final int n = coordinates.length;
final int m = n >>> 1;
-- if (n != other.coordinates.length) {
-- throw new MismatchedDimensionException(Errors.format(
-- Errors.Keys.MismatchedDimension_3, "other", m, other.getDimension()));
-- }
-- // First condition below is a fast check for a common case.
-- if (types != other.types && types != null && other.types != null) {
-- for (int i=0; i<m; i++) {
-- final DimensionNameType t1 = types[i];
-- if (t1 != null) {
-- final DimensionNameType t2 = other.types[i];
-- if (t2 != null && !t1.equals(t2)) {
-- throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedAxes_3, i, t1, t2));
-- }
-- }
-- }
-- }
final long[] clipped = new long[n];
int i = 0;
while (i < m) {clipped[i] = extremum(coordinates[i], other.coordinates[i], !union); i++;}
@@@ -1803,6 -1803,6 +1834,33 @@@
return max ? Math.max(a, b) : Math.min(a, b);
}
++ /**
++ * Ensures that the given grid extent has the same number of dimensions and the same axes than this grid extent.
++ * Only axis names that are specified in both extents are compared. Unspecified names are interpreted as unknown,
++ * which are conservatively interpreted as a match.
++ *
++ * @param other grid extent for which to compare the number of dimensions and the axis names.
++ * @param param name of the {@code other} parameter, used for formatting the message if an exception is thrown.
++ * @throws IllegalArgumentException if the number of dimensions or at least one axis name does not match.
++ */
++ final void ensureSameAxes(final GridExtent other, final String param) {
++ final int n = coordinates.length;
++ final int m = n >>> 1;
++ if (n != other.coordinates.length) {
++ throw new MismatchedDimensionException(Errors.format(
++ Errors.Keys.MismatchedDimension_3, param, m, other.getDimension()));
++ }
++ // First condition below is a fast check for a common case.
++ if (types != other.types && types != null && other.types != null) {
++ for (int i=0; i<m; i++) {
++ final DimensionNameType t1, t2;
++ if ((t1 = types[i]) != null && (t2 = other.types[i]) != null && !t1.equals(t2)) {
++ throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedAxes_3, i, t1, t2));
++ }
++ }
++ }
++ }
++
/**
* Returns whether this grid extent has the same size than the given extent.
* If the given extent is {@code null} or has a different number of dimensions,
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ReducedGridCoverage.java
index 0000000000,0000000000..77c6a3d3ba
new file mode 100644
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ReducedGridCoverage.java
@@@ -1,0 -1,0 +1,160 @@@
++/*
++ * 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.TreeMap;
++import java.awt.image.RenderedImage;
++import org.opengis.geometry.DirectPosition;
++import org.apache.sis.internal.util.CollectionsExt;
++
++
++/**
++ * 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, which usually implies
++ * a reduction in the number of dimensions of the CRS but not necessarily in same order.
++ * The sample dimensions (coverage range) are unmodified.
++ *
++ * <p>This coverage is a <em>view</em>: changes in the source coverage are reflected
++ * immediately in this {@code ReducedGridCoverage}, and conversely.</p>
++ *
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ * @since 1.4
++ */
++final class ReducedGridCoverage extends DerivedGridCoverage {
++ /**
++ * The dimensionality reduction to apply on the domain of the source coverage.
++ */
++ private final DimensionalityReduction reduction;
++
++ /**
++ * Creates a new reduced grid coverage.
++ *
++ * @param source the source coverage.
++ * @param reduction the dimensionality reduction to apply on the source coverage.
++ */
++ ReducedGridCoverage(final GridCoverage source, final DimensionalityReduction reduction) {
++ super(source, reduction.getReducedGridGeometry());
++ this.reduction = reduction;
++ }
++
++ /**
++ * Specifies that this coverage should not be replaced by its source.
++ * This is because the number of grid dimensions is not the same.
++ */
++ @Override
++ final boolean isNotRepleacable() {
++ return true;
++ }
++
++ /**
++ * Returns a two-dimensional slice of grid data as a rendered image.
++ * This method delegates to the source coverage with a slice extent completed
++ * with the dimensions that were hidden by this {@code ReducedGridCoverage}.
++ *
++ * @param sliceExtent a subspace of this grid coverage, or {@code null} for the whole coverage.
++ * @return the grid slice as a rendered image. Image location is relative to {@code sliceExtent}.
++ */
++ @Override
++ public RenderedImage render(final GridExtent sliceExtent) {
++ return source.render(reduction.reverse(sliceExtent));
++ }
++
++ /**
++ * Creates a new function for computing or interpolating sample values at given locations.
++ * That function accepts {@link DirectPosition} in arbitrary Coordinate Reference System.
++ * If a given position contains coordinate values for dimensions that were hidden by this
++ * {@code ReducedGridCoverage}, the specified position coordinates will have precedence
++ * over the dimensions that were specified in the {@link DimensionalityReduction} object.
++ */
++ @Override
++ public Evaluator evaluator() {
++ return new SliceEvaluator(source.evaluator());
++ }
++
++ /**
++ * Implementation of the evaluator returned by {@link #evaluator()}.
++ * This evaluator delegates to the source evaluator with default slices
++ * initialized to the values specified by the {@link DimensionalityReduction}.
++ */
++ private final class SliceEvaluator extends EvaluatorWrapper {
++ /**
++ * The slice where to perform evaluation, or {@code null} if not yet computed.
++ * This is the cached value of {@link #getDefaultSlice()}.
++ */
++ private Map<Integer,Long> slice;
++
++ /**
++ * Creates a new evaluator wrapping the given source coverage evaluator.
++ *
++ * @param source an evaluator newly created from the source coverage.
++ */
++ SliceEvaluator(final Evaluator source) {
++ super(source);
++ setDefaultSlice(null);
++ }
++
++ /**
++ * Returns the coverage from which this evaluator is fetching sample values.
++ * This is the coverage on which the {@link ReducedGridCoverage#evaluator()}
++ * method has been invoked.
++ */
++ @Override
++ public GridCoverage getCoverage() {
++ return ReducedGridCoverage.this;
++ }
++
++ /**
++ * Returns the default slice where to perform evaluation, or an empty map if unspecified.
++ */
++ @Override
++ @SuppressWarnings("ReturnOfCollectionOrArrayField") // Because the map is unmodifiable.
++ public Map<Integer,Long> getDefaultSlice() {
++ if (slice == null) {
++ final var copy = new TreeMap<Integer,Long>();
++ super.getDefaultSlice().forEach((dim, coord) -> {
++ dim = reduction.toReducedDimension(dim);
++ if (dim >= 0) copy.put(dim, coord);
++ });
++ slice = CollectionsExt.unmodifiableOrCopy(copy);
++ }
++ return slice;
++ }
++
++ /**
++ * Sets the default slice where to perform evaluation when the points do not have enough dimensions.
++ *
++ * @throws IllegalArgumentException if the map contains an illegal dimension or grid coordinate value.
++ */
++ @Override
++ public void setDefaultSlice(Map<Integer,Long> slice) {
++ if (slice == null) {
++ GridGeometry origin = ReducedGridCoverage.this.source.getGridGeometry();
++ slice = origin.getExtent().getSliceCoordinates();
++ } else {
++ final var copy = new TreeMap<Integer,Long>();
++ slice.forEach((dim, coord) -> copy.put(reduction.toSourceDimension(dim), coord));
++ slice = copy;
++ }
++ slice.putAll(reduction.getSliceCoordinates());
++ super.setDefaultSlice(slice);
++ this.slice = null;
++ }
++ }
++}
diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/SliceGeometry.java
index 661689ad2a,661689ad2a..dc57a98663
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/SliceGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/SliceGeometry.java
@@@ -48,7 -48,7 +48,7 @@@ import org.apache.sis.util.resources.Er
* That function is invoked (indirectly) by {@link org.apache.sis.internal.coverage.j2d.TiledImage#getProperty(String)}.</p>
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.3
++ * @version 1.4
* @since 1.1
*/
final class SliceGeometry implements Function<RenderedImage, GridGeometry> {
@@@ -68,6 -68,6 +68,15 @@@
*/
private final int[] gridDimensions;
++ /**
++ * Dimensions of the slices in the CRS space, or {@code null} if not yet computed.
++ * This is computed as a side-product of {@link #reduce(GridExtent, int)} method call.
++ * This is often the same dimensions than {@link #gridDimensions}, but not necessarily.
++ *
++ * @see #getTargetDimensions()
++ */
++ private int[] crsDimensions;
++
/**
* Factory to use for creating new transforms, or {@code null} for default.
*/
@@@ -143,7 -143,7 +152,7 @@@
* elements if `TransformSeparator` detected that dropping a grid dimension does not force us to drop
* the corresponding CRS dimension, for example because it has a constant value.
*/
-- int[] crsDimensions = gridDimensions;
++ crsDimensions = gridDimensions;
if (gridToCRS != null) {
TransformSeparator sep = new TransformSeparator(gridToCRS, factory);
sep.addSourceDimensions(gridDimensions);
@@@ -360,6 -360,6 +369,16 @@@
return crsDimensions;
}
++ /**
++ * Returns the dimensions in the slice in the CRS space.
++ * This is a side-product of {@link #reduce(GridExtent, int)}.
++ * Callers should not modify the returned array since it is not cloned.
++ */
++ @SuppressWarnings("ReturnOfCollectionOrArrayField")
++ final int[] getTargetDimensions() {
++ return crsDimensions;
++ }
++
/**
* Invoked if an error occurred while computing the {@link ImageRenderer#getImageGeometry(int)} value.
* This exception should never occur actually, unless a custom factory implementation is used
diff --cc core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
index 0000000000,924fae4a6f..d0abc2f628
mode 000000,100644..100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
@@@ -1,0 -1,439 +1,190 @@@
++/*
++ * 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.image;
+
-import java.awt.Dimension;
-import java.awt.Point;
++import java.util.Arrays;
++import java.util.Objects;
+ import java.awt.Rectangle;
-import java.awt.geom.Point2D;
-import java.awt.image.BandedSampleModel;
+ import java.awt.image.ColorModel;
-import java.awt.image.DataBuffer;
+ import java.awt.image.Raster;
+ import java.awt.image.RenderedImage;
-import java.awt.image.SampleModel;
+ import java.awt.image.WritableRaster;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Function;
-import java.util.stream.IntStream;
-import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
-import org.apache.sis.util.ArgumentChecks;
-
-import static java.lang.Math.multiplyExact;
-
++import org.apache.sis.util.ArraysExt;
++import org.apache.sis.util.Workaround;
++import org.apache.sis.internal.coverage.j2d.ImageUtilities;
++
++
++/**
++ * An image where each band is taken from a selection of bands in a sequence of source images.
++ * This image will share the underlying data arrays when possible, or copy bands otherwise.
++ * The actual strategy may be a mix of both bands copying and sharing.
++ *
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ *
++ * @see BandSelectImage
++ * @see ImageCombiner
++ *
++ * @since 1.4
++ */
+ final class BandAggregateImage extends ComputedImage {
++ /**
++ * The source images with only the bands to aggregate, in order.
++ * Those images are views; the band sample values are not copied.
++ */
++ private final RenderedImage[] filteredSources;
+
- private final Specification spec;
-
- private BandAggregateImage(Specification spec) {
- super(spec.tileModel, spec.sources.stream().map(TileSource::getSource).toArray(RenderedImage[]::new));
- this.spec = spec;
- }
-
- @Override
- protected Raster computeTile(int tileX, int tileY, WritableRaster previous) throws Exception {
- final Point tileLocation = tileToPixel(tileX, tileY);
- return spec.strategy.aggregate(tileX, tileY, tileLocation, spec);
- }
-
- @Override
- public ColorModel getColorModel() { return spec.aggregationColors; }
-
- @Override
- public int getWidth() {
- return spec.pixelRegion.width;
- }
-
- @Override
- public int getHeight() {
- return spec.pixelRegion.height;
- }
-
- @Override
- public int getMinX() { return spec.pixelRegion.x; }
-
- @Override
- public int getMinY() { return spec.pixelRegion.y; }
-
- @Override
- public int getMinTileX() { return spec.tileDisposition.x; }
-
- @Override
- public int getMinTileY() { return spec.tileDisposition.y; }
-
- @Override
- public int getNumXTiles() { return spec.tileDisposition.width; }
-
- @Override
- public int getNumYTiles() { return spec.tileDisposition.height; }
-
- private Point tileToPixel(int tileX, int tileY) {
- int pX = this.getMinX() + multiplyExact((tileX - getMinTileX()), getTileWidth());
- int pY = this.getMinY() + multiplyExact((tileY - getMinTileY()), getTileHeight());
- return new Point(pX, pY);
- }
-
- /*
- * FACTORY METHODS
++ /**
++ * Color model of the aggregated image.
++ *
++ * @see #getColorModel()
+ */
++ private final ColorModel colorModel;
+
- static RenderedImage aggregateBands(RenderedImage[] sources, int[][] bandsToPreserve, ColorModel userColorModel) {
- final ContextInformation info = parseAndValidateInput(sources, bandsToPreserve, userColorModel);
- return tryTileOptimizedStrategy(info)
- .rightOr(reason
- -> fallbackStrategy(info)
- .mapLeft(otherReason -> "Reasons: " + reason + ", " + otherReason)
- )
- .mapRight(BandAggregateImage::new)
- .getRightOrThrow(IllegalArgumentException::new);
- }
++ /**
++ * Domain of pixel coordinates. All images shall share the same pixel coordinate space,
++ * meaning that a pixel at coordinates (<var>x</var>, <var>y</var>) in this image will
++ * contain the sample values of all source images at the same coordinates.
++ * It does <em>not</em> mean that all source images shall have the same bounds.
++ */
++ private final int minX, minY, width, height;
+
- /*
- * PRIVATE STATIC METHODS
++ /**
++ * Index of the first tile. Contrarily to pixel coordinates,
++ * the tile coordinate space does not need to be the same for all images.
+ */
++ private final int minTileX, minTileY;
+
+ /**
- * Initial analysis of input images to aggregate. Note that this method aims to make source information more
- * accessible and easy to use before further processing. It also try to detect incompatibilities early, to
- * raise meaningful errors for users.
- * <p>
- * Note: crunching data into a more dense/accessible shape aims to ease further analysis/optimisations. This should
- * allow more lisible and less coupled code, to ease setup of strategies, readability and maintenance.
++ * Creates a new aggregation of bands.
++ * This static method is a workaround for RFE #4093999
++ * ("Relax constraint on placement of this()/super() call in constructors").
+ *
- * @param sources images to aggregate, in order.
- * @param bandsToPreserve Bands to use for each image, in order. Holds same contract as the {@link #aggregateBands(RenderedImage[], int[][], ColorModel) factory method}.
- * @param userColorModel
- * @return Parsed information about data sources.
- * @throws IllegalArgumentException If we detect an incompatibility in source images that make them impossible to merge.
++ * @param sources images to combine, in order.
++ * @param bandsPerSource bands to use for each source image, in order. May contain {@code null} elements.
++ * @param colors the color model to use for this image, or {@code null} for automatic.
++ * @throws IllegalArgumentException if there is an incompatibility between some source images
++ * or if some band indices are duplicated or outside their range of validity.
++ * @return the band aggregate image.
+ */
- private static ContextInformation parseAndValidateInput(RenderedImage[] sources, int[][] bandsToPreserve, ColorModel userColorModel) throws IllegalArgumentException {
- if (bandsToPreserve != null && sources.length > bandsToPreserve.length) throw new IllegalArgumentException("More band selections than source images are provided.");
- if (sources.length < 2) throw new IllegalArgumentException("At least two images are required for band aggregation. For band selection on a single image, please use dedicated utility");
-
- List<Rectangle> domains = new ArrayList<>(sources.length);
- List<SourceSelection> sourcesWithBands = new ArrayList<>(sources.length);
- int commonDataType, minTileWidth, minTileWidthIdx, minTileHeight, minTileHeightIdx;
- RenderedImage source = sources[0];
- int[] bands = bandsToPreserve == null || bandsToPreserve.length < 1 ? null : bandsToPreserve[0];
- SampleModel sourceSM = source.getSampleModel();
- commonDataType = sourceSM.getDataType();
- int numBands = validateAndCountBands(bands, sourceSM);
- sourcesWithBands.add(new SourceSelection(source, bands));
- minTileWidthIdx = minTileHeightIdx = 0;
- minTileWidth = source.getTileWidth();
- minTileHeight = source.getTileHeight();
- domains.add(new Rectangle(source.getMinX(), source.getMinY(), source.getWidth(), source.getHeight()));
- for (int i = 1 ; i < sources.length ; i++) {
- source = sources[i];
- sourceSM = source.getSampleModel();
- int dataType = sourceSM.getDataType();
- if (dataType != commonDataType) throw new IllegalArgumentException("Images to merge define different data types. This is not supported. Please align all images on a single data-type beforehand.");
-
- bands = bandsToPreserve == null || bandsToPreserve.length <= i ? null : bandsToPreserve[i];
- numBands += validateAndCountBands(bands, sourceSM);
-
- sourcesWithBands.add(new SourceSelection(source, bands));
-
- domains.add(new Rectangle(source.getMinX(), source.getMinY(), source.getWidth(), source.getHeight()));
-
- if (minTileWidth > source.getTileWidth()) {
- minTileWidth = source.getTileWidth();
- minTileWidthIdx = i;
++ @Workaround(library="JDK", version="1.8")
++ static RenderedImage create(RenderedImage[] sources, int[][] bandsPerSource, ColorModel colors) {
++ var image = new BandAggregateImage(CombinedImageLayout.create(sources, bandsPerSource), colors);
++ if (image.filteredSources.length == 1) {
++ final RenderedImage c = image.filteredSources[0];
++ if (image.colorModel == null) {
++ return c;
+ }
- if (minTileHeight > source.getTileHeight()) {
- minTileHeight = source.getTileHeight();
- minTileHeightIdx = i;
++ final ColorModel cm = c.getColorModel();
++ if (cm == null || image.colorModel.equals(cm)) {
++ return c;
+ }
+ }
-
- // TODO: with current information, we should be able to adapt domain definition to a user configuration : intersection, union, strict.
- // User could specify if he wants band aggregation image to cover the intersection or union of input images.
- // "strict" mode would serve to prevent band aggregation on images using different domains (raise an error if domains are not the same).
- // For now, we will use intersection.
- final Rectangle intersection = domains.stream()
- .reduce(Rectangle::intersection)
- .filter(it -> !it.isEmpty())
- .orElseThrow(() -> new IllegalArgumentException("source images do not intersect."));
-
- return new ContextInformation(commonDataType, numBands, minTileWidthIdx, minTileHeightIdx, domains, intersection, sourcesWithBands, userColorModel);
- }
-
- private static int validateAndCountBands(int[] bandSelection, SampleModel model) {
- final int numBands = model.getNumBands();
- if (bandSelection == null || bandSelection.length < 1) return numBands;
- for (int band : bandSelection) ArgumentChecks.ensureValidIndex(numBands, band);
- return bandSelection.length;
- }
-
- private static Either<String, Specification> tryTileOptimizedStrategy(ContextInformation context) {
- final RenderedImage tilingX = context.sources.get(context.minTileWidthIndex).image;
- final int tileWidth = tilingX.getTileWidth();
- final int minTileX = tilingX.getMinTileX();
-
- final RenderedImage tilingY = context.sources.get(context.minTileHeightIndex).image;
- final int tileHeight = tilingY.getTileHeight();
- final int minTileY = tilingY.getMinTileY();
- for (final SourceSelection source : context.sources) {
- final RenderedImage img = source.image;
- if (img.getNumXTiles() > 1 && img.getTileWidth() % tileWidth != 0) {
- return Either.left("no common integer multiplier between sources for tile width");
- }
- else if (img.getNumYTiles() > 1 && img.getTileHeight() % tileHeight != 0) {
- return Either.left("no common integer multiplier between sources for tile height");
- }
-
- if ((img.getMinX() - tilingX.getMinX()) % tileWidth != 0) {
- return Either.left("Tiles are not aligned on X axis");
- }
-
- if ((img.getMinY() - tilingX.getMinY()) % tileHeight != 0) {
- return Either.left("Tiles are not aligned on Y axis");
- }
- }
-
- // Ensure domain covers entire tiles.
- final Rectangle pixelDomain = context.intersection;
- assert pixelDomain.width % tileWidth == 0 : "Computed pixel domain does not align with tile borders";
- assert pixelDomain.height % tileHeight == 0 : "Computed pixel domain does not align with tile borders";
-
- TileAlignedSource[] preparedSources = new TileAlignedSource[context.sources.size()];
- for (int i = 0 ; i < preparedSources.length ; i++) {
- final SourceSelection source = context.sources.get(i);
- preparedSources[i] = new TileAlignedSource(source.image, source.bands);
- }
-
- final SampleModel tileModel = new BandedSampleModel(context.commonDataType, tileWidth, tileHeight, context.outputBandNumber);
-
- Rectangle tileDisposition = new Rectangle(minTileX, minTileY, pixelDomain.width / tileWidth, pixelDomain.height / tileHeight);
- ColorModel outColorModel = context.userColorModel;
- if (outColorModel == null) outColorModel = createColorModel(context);
- else if (!context.userColorModel.isCompatibleSampleModel(tileModel)) {
- throw new IllegalArgumentException("User color model is not compatible with band aggregation sample model. Please provide a banded color model.");
- }
-
- return Either.right(new Specification(Collections.unmodifiableList(Arrays.asList(preparedSources)), outColorModel, tileModel, pixelDomain, tileDisposition, new TileCopy()));
++ return ImageProcessor.unique(image);
+ }
+
+ /**
- * Approximate guess of the output color model:
- * <ol>
- * <li>
- * If aggregation result is 3 or 4 bands, and data type is byte or short, we create a RGB color model.
- * If there's 4 bands, an RGBA color model is defined.
- * </li>
- * <li>Otherwise, if the first image is already single banded, we return directly its color model (if non null)</li>
- * <li>As a last resort, a greyscale color model is made, that try to "guess" value range from the data-type.</li>
- * </ol>
++ * Creates a new aggregation of bands.
++ *
++ * @param layout pixel and tile coordinate spaces of this image, together with sample model.
++ * @param colors the color model to use for this image, or {@code null} for automatic.
+ */
- private static ColorModel createColorModel(ContextInformation context) {
- if (context.outputBandNumber == 3 || context.outputBandNumber == 4) {
- switch (context.commonDataType) {
- case DataBuffer.TYPE_BYTE:
- case DataBuffer.TYPE_SHORT:
- return ColorModelFactory.createRGB(context.commonDataType * Byte.SIZE, false, context.outputBandNumber == 4);
- }
- }
-
- final SourceSelection first = context.sources.get(0);
- if (first.image.getSampleModel().getNumBands() == 1 && first.image.getColorModel() != null) {
- return first.image.getColorModel();
- }
-
- final double vmin, vmax;
- switch (context.commonDataType) {
- case DataBuffer.TYPE_BYTE: vmin = 0 ; vmax = 255 ; break;
- case DataBuffer.TYPE_SHORT: vmin = Short.MIN_VALUE ; vmax = Short.MAX_VALUE ; break;
- case DataBuffer.TYPE_USHORT: vmin = 0 ; vmax = 65535 ; break;
- case DataBuffer.TYPE_INT: vmin = 0 ; vmax = Integer.MAX_VALUE ; break;
- default: vmin = 0.0 ; vmax = 1.0;
- }
-
- return ColorModelFactory.createGrayScale(context.commonDataType, 1, 0, vmin, vmax);
- }
-
- private static Either<String, Specification> fallbackStrategy(ContextInformation info) {
- return Either.left("No fallback strategy yet");
- }
++ private BandAggregateImage(final CombinedImageLayout layout, ColorModel colors) {
++ super(layout.sampleModel, layout.sources);
++ final Rectangle r = layout.domain;
++ minX = r.x;
++ minY = r.y;
++ width = r.width;
++ height = r.height;
++ minTileX = layout.minTileX;
++ minTileY = layout.minTileY;
++ if (colors == null) {
++ colors = layout.createColorModel();
++ } else {
++ layout.ensureCompatible("colors", colors);
++ }
++ colorModel = colors;
++ filteredSources = layout.getFilteredSources();
++ }
++
++ /** Returns the information inferred at construction time. */
++ @Override public ColorModel getColorModel() {return colorModel;}
++ @Override public int getWidth() {return width;}
++ @Override public int getHeight() {return height;}
++ @Override public int getMinX() {return minX;}
++ @Override public int getMinY() {return minY;}
++ @Override public int getMinTileX() {return minTileX;}
++ @Override public int getMinTileY() {return minTileY;}
+
+ /**
- * INTERNAL TYPES
++ * Creates a raster sharing containing a copy of the selected bands in source images.
++ *
++ * @param tileX the column index of the tile to compute.
++ * @param tileY the row index of the tile to compute.
++ * @param previous the previous tile, reused if non-null.
++ *
++ * @todo Share data arrays instead of copying when possible.
+ */
-
- private static final class SourceSelection {
- final RenderedImage image;
- final int[] bands;
-
- private SourceSelection(RenderedImage image, int[] bands) {
- this.image = image;
- this.bands = bands == null ? null : bands.clone();
- }
- }
-
- private static class ContextInformation {
- /**
- * DataBuffer type used by all sources without exception. If sources used different datatypes, this is -1.
- */
- final int commonDataType;
- /**
- * Number of bands resulting from the image merge.
- */
- final int outputBandNumber;
-
- /**
- * Index of source image providing minimal tile <em>width</em>
- */
- final int minTileWidthIndex;
-
- /**
- * Index of source image providing minimal tile <em>height</em>
- */
- final int minTileHeightIndex;
-
- /**
- * Commodity attribute: provides source images <em>pixel</em> boundaries, in order.
- */
- final List<Rectangle> sourcePxDomains;
-
- /**
- * Intersection of all {@link #sourcePxDomains source pixel domains}.
- */
- private final Rectangle intersection;
-
- final List<SourceSelection> sources;
-
- final ColorModel userColorModel;
-
- public ContextInformation(int commonDataType, int outputBandNumber, int minTileWidthIndex, int minTileHeightIndex, List<Rectangle> sourcePxDomains, Rectangle intersection, List<SourceSelection> sources, ColorModel userColorModel) {
- this.commonDataType = commonDataType;
- this.outputBandNumber = outputBandNumber;
- this.minTileWidthIndex = minTileWidthIndex;
- this.minTileHeightIndex = minTileHeightIndex;
- this.sourcePxDomains = Collections.unmodifiableList(new ArrayList<>(sourcePxDomains));
- this.intersection = intersection;
- this.sources = Collections.unmodifiableList(new ArrayList<>(sources));
- this.userColorModel = userColorModel;
++ @Override
++ protected Raster computeTile(final int tileX, final int tileY, WritableRaster tile) {
++ if (tile == null) {
++ tile = createTile(tileX, tileY);
+ }
- }
-
- static class Specification {
- final List<TileSource> sources;
- final ColorModel aggregationColors;
- final SampleModel tileModel;
-
- final Rectangle pixelRegion;
- final Rectangle tileDisposition;
- final BandAggregationStrategy strategy;
-
- public Specification(List<TileSource> sources, ColorModel aggregationColors, SampleModel tileModel, Rectangle pixelRegion, Rectangle tileDisposition, BandAggregationStrategy strategy) {
- this.sources = sources;
- this.aggregationColors = aggregationColors;
- this.tileModel = tileModel;
- this.pixelRegion = pixelRegion;
- this.tileDisposition = tileDisposition;
- this.strategy = strategy;
++ int band = 0;
++ for (final RenderedImage source : filteredSources) {
++ final Rectangle aoi = tile.getBounds();
++ ImageUtilities.clipBounds(source, aoi);
++ final int numBands = ImageUtilities.getNumBands(source);
++ final int[] bands = ArraysExt.range(band, band + numBands);
++ var target = tile.createWritableChild(aoi.x, aoi.y, aoi.width, aoi.height,
++ aoi.x, aoi.y, bands);
++ band += numBands;
++ copyData(aoi, source, target);
+ }
++ return tile;
+ }
+
+ /**
- * For now, only available tile source is "aligned", i.e. sources providing tiles covering computed image tiles.
- * In the future, another implementation capable of providing Raster built from multiple crossing tiles could be
- * done, to extend band aggregation capabilities.
++ * Returns a hash code value for this image.
+ */
- interface TileSource {
- RenderedImage getSource();
-
- /**
- * Return a raster aligned with <em>parent computed tile</em> described by input offset.
- * @param computedTileX X coordinate of the <em>parent computed tile</em>.
- * @param computedTileY Y coordinate of the <em>parent computed tile</em>.
- * @return A raster whose domain match output computed tile.
- */
- Raster getTile(int computedTileX, int computedTileY, Rectangle computedTileRegion);
++ @Override
++ public int hashCode() {
++ return sampleModel.hashCode() + 37 * (Arrays.hashCode(filteredSources) + 31 * Objects.hashCode(colorModel));
+ }
+
+ /**
- * WARNING: only work with sources whose tile size is a multiple of computed image tiles, i.e: All and any tiles in
- * computed images MUST be contained in one and only one source tile.
++ * Compares the given object with this image for equality.
++ *
++ * <h4>Implementation note</h4>
++ * We do not invoke {@link #equalsBase(Object)} for saving the comparisons of {@link ComputedImage#sources} array.
++ * The comparison of {@link #filteredSources} array will indirectly include the comparison of raw source images.
+ */
- private static class TileAlignedSource implements TileSource {
-
- private final RenderedImage source;
-
- /**
- * Null if all bands in the image are selected. Otherwise,ordered (not necessarily sorted) indices of selected bands.
- */
- private final int[] selectedBands;
-
- public TileAlignedSource(RenderedImage source, int[] selectedBands) {
- this.source = source;
- this.selectedBands = selectedBands;
- }
-
- @Override
- public RenderedImage getSource() {
- return source;
- }
-
- @Override
- public Raster getTile(int computedTileX, int computedTileY, Rectangle computedTileRegion) {
- final double tX = source.getMinTileX() + (computedTileRegion.x - source.getMinX()) / (double) source.getTileWidth();
- final double tY = source.getMinTileY() + (computedTileRegion.y - source.getMinY()) / (double) source.getTileHeight();
- assert regionsAligned(computedTileRegion, new Point2D.Double(tX, tY))
- : "Source tiles do not align with computed image tiles";
- final Raster sourceTile = source.getTile((int) Math.floor(tX), (int) Math.floor(tY));
- return sourceTile.createChild(computedTileRegion.x, computedTileRegion.y, computedTileRegion.width, computedTileRegion.height, computedTileRegion.x, computedTileRegion.y, selectedBands);
- }
-
- private boolean regionsAligned(Rectangle computedTileRegion, Point2D origin) {
- final double sourceX = origin.getX() * source.getTileWidth() + source.getTileGridXOffset();
- final double sourceY = origin.getY() * source.getTileHeight() + source.getTileGridYOffset();
- return Math.abs(sourceX - computedTileRegion.x) < 1 && Math.abs(sourceY - computedTileRegion.y) < 1;
- }
- }
-
- @FunctionalInterface
- interface BandAggregationStrategy {
- Raster aggregate(int tileX, int tileY, Point tilePixelOrigin, BandAggregateImage.Specification spec);
- }
-
- private static class TileCopy implements BandAggregationStrategy {
-
- @Override
- public Raster aggregate(int tileX, int tileY, Point tilePixelOrigin, BandAggregateImage.Specification spec) {
- final WritableRaster output = WritableRaster.createWritableRaster(spec.tileModel, tilePixelOrigin);
- int i = 0;
- for (TileSource source : spec.sources) {
- Raster sourceRaster = source.getTile(tileX, tileY, new Rectangle(tilePixelOrigin, new Dimension(spec.tileModel.getWidth(), spec.tileModel.getHeight())));
- final int sourceNumBands = sourceRaster.getNumBands();
- final int[] targetBands = IntStream.range(i, i + sourceNumBands).toArray();
- WritableRaster outputBandsOfInterest = output.createWritableChild(output.getMinX(), output.getMinY(), output.getWidth(), output.getHeight(), output.getMinX(), output.getMinY(), targetBands);
- outputBandsOfInterest.setRect(sourceRaster);
- i += sourceNumBands;
- }
-
- assert i == spec.tileModel.getNumBands() : "Mismatch: unexpected number of transferred bands";
-
- return output;
- }
- }
-
- private static class Either<L, R> {
-
- private final L left;
- private final R right;
-
- Either(L left, R right) {
- if (left == null && right == null || left != null && right != null) throw new IllegalArgumentException("Expect exactly one non-null branch");
- this.left = left;
- this.right = right;
- }
-
- <V> Either<L, V> mapRight(Function<? super R, ? extends V> transformRightToValue) { return left == null ? right(transformRightToValue.apply(right)) : (Either<L, V>) this; }
- <V> Either<V, R> mapLeft(Function<? super L, ? extends V> transformLeftToValue) { return right == null ? left(transformLeftToValue.apply(left)) : (Either<V, R>) this; }
- public Either<L, R> rightOr(Function<? super L, Either<L, R>> recover) { return right == null ? recover.apply(left) : this; }
- public <E extends Exception> R getRightOrThrow(Function<L, E> createError) throws E { if (right != null) return right; else throw createError.apply(left); }
- static <L, R> Either<L, R> left(L leftValue) { return new Either<>(leftValue, null); }
- static <L, R> Either<L, R> right(R rightValue) { return new Either<>(null, rightValue); }
++ @Override
++ public boolean equals(final Object object) {
++ if (object instanceof BandAggregateImage) {
++ final BandAggregateImage other = (BandAggregateImage) object;
++ return minTileX == other.minTileX &&
++ minTileY == other.minTileY &&
++ getBounds().equals(other.getBounds()) &&
++ sampleModel.equals(other.sampleModel) &&
++ Objects.equals(colorModel, other.colorModel) &&
++ Arrays.equals(filteredSources, other.filteredSources);
++ }
++ return false;
+ }
+ }
diff --cc core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
index 5783e6c769,5783e6c769..7a13fb98d3
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
@@@ -33,11 -33,11 +33,14 @@@ import org.apache.sis.internal.coverage
/**
-- * Selects or reorder bands from a source image. This operation avoid copying sample values;
-- * it works by modifying the sample model and color model.
++ * Selects or reorder bands from a source image.
++ * This operation never copies sample values.
++ * Instead, it works by modifying the sample model and color model.
++ * This economical behavior is important for implementation of other
++ * operations on top of this one, such as {@link BandAggregateImage}.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.3
++ * @version 1.4
* @since 1.1
*/
final class BandSelectImage extends SourceAlignedImage {
@@@ -73,7 -73,7 +76,8 @@@
* Creates a new "band select" operation for the given source.
*
* @param source the image in which to select bands.
-- * @param bands the bands to select.
++ * @param bands the bands to select. Should be a clone of user-specified argument
++ * for protection against user changes in the given array.
*/
static RenderedImage create(final RenderedImage source, final int[] bands) {
final int numBands = ImageUtilities.getNumBands(source);
@@@ -93,6 -93,6 +97,7 @@@
* hoping user won't need the color model. We could have tried to create an arbitrary model,
* but it is difficult to know if it would do more good than harm.
*/
++ final RenderedImage image;
if (cm != null && source instanceof BufferedImage) {
final BufferedImage bi = (BufferedImage) source;
@SuppressWarnings("UseOfObsoleteCollectionType")
@@@ -103,11 -103,11 +108,13 @@@
properties.put(key, value);
}
}
-- return new BufferedImage(cm,
++ image = new BufferedImage(cm,
bi.getRaster().createWritableChild(0, 0, bi.getWidth(), bi.getHeight(), 0, 0, bands),
bi.isAlphaPremultiplied(), properties);
++ } else {
++ image = new BandSelectImage(source, cm, bands);
}
-- return new BandSelectImage(source, cm, bands.clone());
++ return ImageProcessor.unique(image);
}
/**
diff --cc core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
index 0000000000,0000000000..fe9e5a8939
new file mode 100644
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
@@@ -1,0 -1,0 +1,366 @@@
++/*
++ * 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.image;
++
++import java.awt.Point;
++import java.awt.Dimension;
++import java.awt.Rectangle;
++import java.awt.image.ColorModel;
++import java.awt.image.DataBuffer;
++import java.awt.image.RenderedImage;
++import java.awt.image.SampleModel;
++import org.apache.sis.util.Workaround;
++import org.apache.sis.util.resources.Errors;
++import org.apache.sis.util.collection.FrequencySortedSet;
++import org.apache.sis.internal.feature.Resources;
++import org.apache.sis.internal.coverage.j2d.ImageLayout;
++import org.apache.sis.internal.coverage.j2d.ImageUtilities;
++import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
++import org.apache.sis.internal.coverage.MultiSourcesArgument;
++import org.apache.sis.coverage.grid.DisjointExtentException;
++
++
++/**
++ * Computes the bounds of a destination image which will combine many source images.
++ * A combination may be an aggregation or an overlay of bands, depending on the image class.
++ * All images are assumed to use the same pixel coordinate space: (<var>x</var>, <var>y</var>)
++ * expressed in pixel coordinates should map to the same geospatial location in all images.
++ *
++ * <p>Instances of this class are temporary and used only during image construction.</p>
++ *
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ *
++ * @see ImageCombiner
++ * @see BandAggregateImage
++ *
++ * @since 1.4
++ */
++final class CombinedImageLayout extends ImageLayout {
++ /**
++ * The source images. This is a copy of the user-specified array,
++ * except that images associated to an empty set of bands are discarded.
++ */
++ final RenderedImage[] sources;
++
++ /**
++ * Ordered (not necessarily sorted) indices of bands to select in each source image.
++ * The length of this array is always equal to the length of the {@link #sources} array.
++ * A {@code null} element means that all bands of the corresponding image should be used.
++ * All non-null elements are non-empty and without duplicated values.
++ */
++ private final int[][] bandsPerSource;
++
++ /**
++ * The sample model of the combined image.
++ */
++ final SampleModel sampleModel;
++
++ /**
++ * The domain of pixel coordinates in the combined image. All sources images are assumed to use
++ * the same pixel coordinate space, i.e. a pixel at coordinates (<var>x</var>, <var>y</var>) in
++ * the combined image will contain values derived from pixels at the same coordinates in all
++ * source images. It does <em>not</em> mean that all source images shall have the same bounds.
++ */
++ final Rectangle domain;
++
++ /**
++ * Index of the first tile. Other tile matrix properties such as the number of tiles and the grid offsets
++ * are derived from those values together with the {@linkplain #domain}. Contrarily to pixel coordinates,
++ * the tile coordinate space does not need to be the same for all images.
++ *
++ * @see #getMinTile()
++ */
++ final int minTileX, minTileY;
++
++ /**
++ * Whether to use the preferred tile size exactly as specified, without trying to compute a better size.
++ * This field may be {@code true} if the tiles of the destination image are at exact same location than
++ * the tiles of a source image having the preferred tile size. In such case, keeping the same size will
++ * reduce the amount of tiles requested in that source image.
++ */
++ private final boolean exactTileSize;
++
++ /**
++ * Computes the layout of an image combining all the specified source images.
++ * The optional {@code bandsPerSource} argument specifies the bands to select in each source images.
++ * That array can be {@code null} for selecting all bands in all source images,
++ * or may contain {@code null} elements for selecting all bands of the corresponding image.
++ * An empty array element (i.e. zero band to select) discards the corresponding source image.
++ *
++ * <p>This static method is a workaround for RFE #4093999
++ * ("Relax constraint on placement of this()/super() call in constructors").
++ * This method may become the constructor after JEP 8300786 is available.</p>
++ *
++ * @param sources images to combine, in order.
++ * @param bandsPerSource bands to use for each source image, in order. May contain {@code null} elements.
++ * @throws IllegalArgumentException if there is an incompatibility between some source images
++ * or if some band indices are duplicated or outside their range of validity.
++ */
++ @Workaround(library="JDK", version="1.8")
++ static CombinedImageLayout create(RenderedImage[] sources, int[][] bandsPerSource) {
++ final var aggregate = new MultiSourcesArgument<RenderedImage>(sources, bandsPerSource);
++ aggregate.identityAsNull();
++ aggregate.validate(ImageUtilities::getNumBands);
++
++ sources = aggregate.sources();
++ bandsPerSource = aggregate.bandsPerSource();
++ Rectangle domain = null;
++ int commonDataType = DataBuffer.TYPE_UNDEFINED;
++ for (final RenderedImage source : sources) {
++ /*
++ * Ensure that all images use the same data type.
++ * Get the domain of the combined image to create.
++ *
++ * TODO: current implementation computes the intersection of all sources.
++ * But a future version should allow users to specify if they want intersection,
++ * union or strict mode instead. A "strict" mode would prevent the combination of
++ * images using different domains (i.e. raise an error if domains are not the same).
++ */
++ final int dataType = source.getSampleModel().getDataType();
++ if (domain == null) {
++ domain = ImageUtilities.getBounds(source);
++ commonDataType = dataType;
++ } else {
++ ImageUtilities.clipBounds(source, domain);
++ if (domain.isEmpty()) {
++ throw new DisjointExtentException(Resources.format(Resources.Keys.SourceImagesDoNotIntersect));
++ }
++ if (dataType != commonDataType) {
++ throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedDataType));
++ }
++ }
++ }
++ if (domain == null) {
++ // `domain` is guaranteed non-null if above block has been executed at least once.
++ throw new IllegalArgumentException(Resources.format(Resources.Keys.UnspecifiedBands));
++ }
++ /*
++ * Tile size is chosen after the domain has been computed, because we prefer a tile size which
++ * is a divisor of the combined image size. Tile sizes of existing source images are preferred,
++ * especially when the tiles are aligned, for increasing the chances that computation a tile of
++ * the combined image causes the computation of a single tile of each source image.
++ */
++ long cx, cy; // A combination of tile size with alignment on the tile matrix grid.
++ cx = cy = (((long) Integer.MAX_VALUE) << Integer.SIZE) | ImageUtilities.DEFAULT_TILE_SIZE;
++ final var tileGridXOffset = new FrequencySortedSet<Integer>(true);
++ final var tileGridYOffset = new FrequencySortedSet<Integer>(true);
++ for (final RenderedImage source : sources) {
++ cx = chooseTileSize(cx, source.getTileWidth(), domain.width, domain.x - source.getMinX());
++ cy = chooseTileSize(cy, source.getTileHeight(), domain.height, domain.y - source.getMinY());
++ tileGridXOffset.add(source.getTileGridXOffset());
++ tileGridYOffset.add(source.getTileGridYOffset());
++ }
++ final var preferredTileSize = new Dimension((int) cx, (int) cy);
++ final boolean exactTileSize = ((cx | cy) >>> Integer.SIZE) == 0;
++ return new CombinedImageLayout(sources, bandsPerSource, domain, preferredTileSize, exactTileSize,
++ chooseMinTile(tileGridXOffset, domain.x, preferredTileSize.width),
++ chooseMinTile(tileGridYOffset, domain.y, preferredTileSize.height),
++ commonDataType, aggregate.numBands());
++ }
++
++ /**
++ * Creates a new image layout from the values computed by {@code create(…)}.
++ *
++ * @param sources images to combine, in order.
++ * @param bandsPerSource bands to use for each source image, in order. May contain {@code null} elements.
++ * @param domain bounds of the image to create.
++ * @param preferredTileSize the preferred tile size.
++ * @param commonDataType data type of the combined image.
++ * @param numBands number of bands of the image to create.
++ */
++ private CombinedImageLayout(final RenderedImage[] sources, final int[][] bandsPerSource,
++ final Rectangle domain, final Dimension preferredTileSize, final boolean exactTileSize,
++ final int minTileX, final int minTileY, final int commonDataType, final int numBands)
++ {
++ super(preferredTileSize, false);
++ this.exactTileSize = exactTileSize;
++ this.bandsPerSource = bandsPerSource;
++ this.sources = sources;
++ this.domain = domain;
++ this.minTileX = minTileX;
++ this.minTileY = minTileY;
++ this.sampleModel = createBandedSampleModel(commonDataType, numBands, null, domain);
++ // Sample model must be last (all other fields must be initialized before).
++ }
++
++ /**
++ * Chooses the preferred tile size (width or height) between two alternatives.
++ * The alternatives are {@code current} and {@code tile}. Note that having the
++ * same tile size than source image is desirable but not mandatory.
++ *
++ * <p>The ideal situation is to have aligned tiles, so that computing one tile of combined image
++ * will always cause the computation of only one tile of source image. For approaching that goal,
++ * we pack the "grid alignment distance" ({@code offset}) in the higher bits and the tile size in
++ * the lower bits of a {@code long}. Minimizing that {@code long} while optimize the offset first,
++ * then choose the smallest tile size between tile matrices at the same offset.</p>
++ *
++ * @param current the current tile size, or 0 if none.
++ * @param tileSize tile size (width or height) of the source image to consider.
++ * @param imageSize full size (width or height) of the source image to consider.
++ * @param offset location of the first pixel of the combined image relative to the first pixel of the source image.
++ * @return the preferred tile size: {@code current} or {@code tile}.
++ */
++ private static long chooseTileSize(final long current, final int tileSize, final int imageSize, final int offset) {
++ if ((imageSize % tileSize) == 0) {
++ long c = Math.abs(offset % tileSize); // How close the grid are aligned (ideal would be zero).
++ c <<= Integer.SIZE; // Pack grid offset in higher bits.
++ c |= tileSize; // Pack tile size in lower bits.
++ /*
++ * The use of `compareUnsigned(…)` below is an opportunistic trick for discarding negative tile size
++ * (should be illegal, but we are paranoiac). If tile size was negative, the first bit should be set,
++ * which is interpreted as a very high value when comparing as unsigned numbers.
++ */
++ if (Long.compareUnsigned(c, current) < 0) {
++ return c;
++ }
++ }
++ return current;
++ }
++
++ /**
++ * Chooses the minimum tile index ({@code minTileX} or {@code minTileY}).
++ * This value does not really matter, it has no incidence on performance or positional accuracy.
++ * However using the same "tile grid offset" in the combined image compared to the source images
++ * can make debugging easier. This is similar to using the same "grid to CRS" transform in grid
++ * coverages.
++ *
++ * @param offsets all "tile grid offsets" sorted with most frequently used first.
++ * @param min the minimal pixel coordinates in the combined image.
++ * @param tileSize the tile size (width or height).
++ * @return suggested minimum tile index.
++ */
++ private static int chooseMinTile(final FrequencySortedSet<Integer> offsets, final int min, final int tileSize) {
++ for (int offset : offsets) {
++ offset = min - offset;
++ if (offset % tileSize == 0) {
++ return offset / tileSize;
++ }
++ }
++ return 0;
++ }
++
++ /**
++ * Returns indices of the first tile ({@code minTileX}, {@code minTileY}).
++ * Other tile matrix properties such as the number of tiles and the grid offsets will be derived
++ * from those values together with the {@linkplain #domain}. Contrarily to pixel coordinates,
++ * the tile coordinate space does not need to be the same for all images.
++ *
++ * @return indices of the first tile ({@code minTileX}, {@code minTileY}).
++ */
++ @Override
++ public Point getMinTile() {
++ return new Point(minTileX, minTileY);
++ }
++
++ /**
++ * Suggests a tile size for the specified image size.
++ * It may be exactly the size of the tiles of a source image, but not necessarily.
++ * This method may compute a tile size different than the tile size of all sources.
++ */
++ @Override
++ public Dimension suggestTileSize(int imageWidth, int imageHeight, boolean allowPartialTiles) {
++ if (exactTileSize) return getPreferredTileSize();
++ return super.suggestTileSize(imageWidth, imageHeight, allowPartialTiles);
++ }
++
++ /**
++ * Suggests a tile size for operations derived from the given image.
++ * It may be exactly the size of the tiles of a source image, but not necessarily.
++ * This method may compute a tile size different than the tile size of all sources.
++ */
++ @Override
++ public Dimension suggestTileSize(RenderedImage image, Rectangle bounds, boolean allowPartialTiles) {
++ if (exactTileSize) return getPreferredTileSize();
++ return super.suggestTileSize(image, bounds, allowPartialTiles);
++ }
++
++ /**
++ * Returns the source images with only the user-specified bands.
++ * The returned images are views; the bands are not copied.
++ *
++ * @return the source images with only user-supplied bands.
++ */
++ final RenderedImage[] getFilteredSources() {
++ final RenderedImage[] images = new RenderedImage[sources.length];
++ for (int i=0; i<images.length; i++) {
++ RenderedImage source = sources[i];
++ final int[] bands = bandsPerSource[i];
++ if (bands != null) {
++ source = BandSelectImage.create(source, bands);
++ }
++ images[i] = source;
++ }
++ return images;
++ }
++
++ /**
++ * Returns the index of the first band which is flagged as a visible band.
++ * The concept of visible bands is used when an image contains more than one band,
++ * but only one of those bands us used by an {@code IndexColorModel}.
++ */
++ private int getVisibleBand() {
++ int band = 0;
++ for (int i=0; i < sources.length; i++) {
++ final RenderedImage source = sources[i];
++ final int[] bands = bandsPerSource[i];
++ final int visibleBand = ImageUtilities.getVisibleBand(source);
++ if (visibleBand >= 0) {
++ if (bands == null) {
++ return band + visibleBand;
++ }
++ for (int j=0; j<bands.length; j++) {
++ if (bands[j] == visibleBand) {
++ return band + j;
++ }
++ }
++ }
++ band += (bands != null) ? bands.length : ImageUtilities.getNumBands(source);
++ }
++ return 0;
++ }
++
++ /**
++ * Builds a default color model.
++ * If the combined image has 3 or 4 bands and the data type is an integer storing values on 8 bits,
++ * then this method creates a RGB color model. If there is 4 bands, an RGBA color model is defined.
++ * Otherwise a grayscale color model is built with a value range inferred from the data-type.
++ */
++ final ColorModel createColorModel() {
++ return ColorModelFactory.createRGB(sampleModel)
++ .orElseGet(() -> ColorModelFactory.createGrayScale(sampleModel, getVisibleBand(), null));
++ }
++
++ /**
++ * Ensures that a user-supplied color model is compatible.
++ *
++ * @param name parameter name of the user-supplied color model.
++ * @param cm the color model to validate. Can be {@code null}.
++ * @throws IllegalArgumentException if the color model is incompatible.
++ */
++ void ensureCompatible(final String name, final ColorModel cm) {
++ final String reason = PlanarImage.verifyCompatibility(sampleModel, cm);
++ if (reason != null) {
++ String message = Resources.format(Resources.Keys.IncompatibleColorModel);
++ if (!reason.isEmpty()) {
++ message = message + ' ' + Errors.format(Errors.Keys.IllegalValueForProperty_2, reason, name);
++ }
++ throw new IllegalArgumentException(message);
++ }
++ }
++}
diff --cc core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index 2ed628303a,49185545bc..793dae2832
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@@ -139,7 -139,7 +139,8 @@@ import org.apache.sis.coverage.grid.Gri
* consider {@linkplain #clone() cloning} if setter methods are invoked on a shared instance.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @author Alexis Manin (Geomatys)
++ * @version 1.4
*
* @see org.apache.sis.coverage.grid.GridCoverageProcessor
*
@@@ -372,8 -372,8 +373,8 @@@ public class ImageProcessor implements
/**
* Sets the colors to use for given categories in image, or {@code null} is unspecified.
* This function provides a way to colorize images without knowing in advance the numerical values of pixels.
-- * For example, instead of specifying <cite>"pixel value 0 in blue, 1 in green, 2 in yellow"</cite>,
-- * this function allows to specify <cite>"Lakes in blue, Forests in green, Sand in yellow"</cite>.
++ * For example, instead of specifying <cite>"pixel value 0 is blue, 1 is green, 2 is yellow"</cite>,
++ * this function allows to specify <cite>"Lakes are blue, Forests are green, Sand is yellow"</cite>.
* It is still possible however to use numerical values if the function desires to do so,
* since this information is available with {@link Category#getSampleRange()}.
*
@@@ -813,7 -813,7 +814,79 @@@
*/
public RenderedImage selectBands(final RenderedImage source, final int... bands) {
ArgumentChecks.ensureNonNull("source", source);
-- return BandSelectImage.create(source, bands);
++ return BandSelectImage.create(source, bands.clone());
++ }
++
++ /**
++ * Aggregates in a single image all the bands of all specified images, in order.
++ * All sources images should map pixel coordinates to the same geospatial locations.
++ * A pixel at coordinates (<var>x</var>, <var>y</var>) in the aggregated image will
++ * contain values from the pixels at the same coordinates in all source images.
++ * The result image will be bounded by the intersection of all source images.
++ *
++ * <h4>Restrictions</h4>
++ * All images shall use the same {@linkplain SampleModel#getDataType() data type},
++ * and all source images shall intersect each other with a non-empty intersection area.
++ * However it is not required that all images have the same bounds or the same tiling scheme.
++ *
++ * <h4>Memory saving</h4>
++ * The returned image may opportunistically share the underlying data arrays of
++ * some source images. Bands are really copied only when sharing is not possible.
++ * The actual strategy may be a mix of both arrays sharing and bands copies.
++ *
++ * <h4>Properties used</h4>
++ * This operation uses the following properties in addition to method parameters:
++ * <ul>
++ * <li>(none)</li>
++ * </ul>
++ *
++ * @param sources images whose bands shall be aggregated, in order. At least one image must be provided.
++ * @return the aggregated image, or {@code sources[0]} returned directly if only one image was supplied.
++ * @throws IllegalArgumentException if there is an incompatibility between some source images.
++ *
++ * @see #aggregateBands(RenderedImage[], int[][], ColorModel)
++ *
++ * @since 1.4
++ */
++ public RenderedImage aggregateBands(final RenderedImage... sources) {
++ return aggregateBands(sources, null, null);
++ }
++
++ /**
++ * Aggregates in a single image the specified bands of a sequence of source images, in order.
++ * This method performs the same work than {@link #aggregateBands(RenderedImage...)},
++ * but with the possibility to specify the bands to retain in each source image.
++ * The {@code bandsPerSource} argument specifies the bands to select in each source image.
++ * That array can be {@code null} for selecting all bands in all source images,
++ * or may contain {@code null} elements for selecting all bands of the corresponding image.
++ * An empty array element (i.e. zero band to select) discards the corresponding source image.
++ * In the latter case, the discarded element in the {@code sources} array may be {@code null}.
++ *
++ * <h4>Restrictions</h4>
++ * <ul>
++ * <li>All images shall use the same {@linkplain SampleModel#getDataType() data type}.</li>
++ * <li>All source images shall intersect each other with a non-empty intersection area.</li>
++ * <li>The same band for a given source image cannot be used twice.</li>
++ * </ul>
++ *
++ * <h4>Properties used</h4>
++ * This operation uses the following properties in addition to method parameters:
++ * <ul>
++ * <li>(none)</li>
++ * </ul>
++ *
++ * @param sources images whose bands shall be aggregated, in order. At least one image must be provided.
++ * @param bandsPerSource bands to use for each source image, in order. May contain {@code null} elements.
++ * @param colors the color model to apply on aggregated image, or {@code null} for inferring a default color model
++ * using aggregated number of bands and sample data type.
++ * @return the aggregated image, or one of the sources if it can be used directly.
++ * @throws IllegalArgumentException if there is an incompatibility between some source images
++ * or if some band indices are duplicated or outside their range of validity.
++ *
++ * @since 1.4
++ */
++ public RenderedImage aggregateBands(RenderedImage[] sources, int[][] bandsPerSource, ColorModel colors) {
++ return BandAggregateImage.create(sources, bandsPerSource, colors);
}
/**
@@@ -1227,6 -1262,6 +1300,7 @@@
final ErrorHandler errorHandler;
final Interpolation interpolation;
final Number[] fillValues;
++ final ImageLayout layout;
final Function<Category,Color[]> colors;
final Quantity<?>[] positionalAccuracyHints;
synchronized (this) {
@@@ -1234,11 -1269,11 +1308,13 @@@
errorHandler = this.errorHandler;
interpolation = this.interpolation;
fillValues = this.fillValues;
++ layout = this.layout;
colors = this.colors;
positionalAccuracyHints = this.positionalAccuracyHints;
}
synchronized (other) {
-- return errorHandler.equals(other.errorHandler) &&
++ return layout.equals(other.layout) &&
++ errorHandler.equals(other.errorHandler) &&
executionMode.equals(other.executionMode) &&
interpolation.equals(other.interpolation) &&
Objects.equals(colors, other.colors) &&
@@@ -1256,8 -1291,8 +1332,8 @@@
*/
@Override
public synchronized int hashCode() {
-- return Objects.hash(getClass(), errorHandler, executionMode, interpolation)
-- + 37 * Arrays.hashCode(fillValues) + 31 * Objects.hashCode(colors)
++ return Objects.hash(getClass(), errorHandler, executionMode, colors, interpolation, layout)
++ + 37 * Arrays.hashCode(fillValues)
+ 39 * Arrays.hashCode(positionalAccuracyHints);
}
diff --cc core/sis-feature/src/main/java/org/apache/sis/image/MaskImage.java
index 95bf133fda,95bf133fda..1d7df7fdb9
--- a/core/sis-feature/src/main/java/org/apache/sis/image/MaskImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MaskImage.java
@@@ -53,7 -53,7 +53,7 @@@ final class MaskImage extends SourceAli
*/
MaskImage(final ResampledImage image) {
super(image, ColorModelFactory.createIndexColorModel(
-- 1, ImageUtilities.getVisibleBand(image), new int[] {0, -1}, true, 0));
++ 1, Math.max(0, ImageUtilities.getVisibleBand(image)), new int[] {0, -1}, true, 0));
MathTransform converter = null;
if (image.interpolation instanceof Visualization.InterpConvert) try {
diff --cc core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index b13324d3c6,b13324d3c6..15b2366401
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@@ -105,7 -105,7 +105,7 @@@ import static java.lang.Math.multiplyFu
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.1
*/
public abstract class PlanarImage implements RenderedImage {
@@@ -423,7 -423,7 +423,7 @@@
public Raster getData() {
final Rectangle aoi = getBounds();
final WritableRaster raster = createWritableRaster(aoi);
-- copyData(aoi, raster);
++ copyData(aoi, this, raster);
return raster;
}
@@@ -442,7 -442,7 +442,7 @@@
throw new IllegalArgumentException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
}
final WritableRaster raster = createWritableRaster(aoi);
-- copyData(aoi, raster);
++ copyData(aoi, this, raster);
return raster;
}
@@@ -466,7 -466,7 +466,7 @@@
raster = createWritableRaster(aoi);
}
if (!aoi.isEmpty()) {
-- copyData(aoi, raster);
++ copyData(aoi, this, raster);
}
return raster;
}
@@@ -476,10 -476,10 +476,11 @@@
* It is caller responsibility to ensure that all arguments are non-null and that the rectangle is contained
* inside both this image and the given raster.
*
-- * @param aoi the region of this image to copy.
-- * @param raster the raster to hold a copy of this image, or {@code null}.
++ * @param aoi the region of the {@code source} image to copy.
++ * @param source the source of pixel data.
++ * @param target the raster to hold a copy of the source image, or {@code null} for creating a new raster.
*/
-- private void copyData(final Rectangle aoi, final WritableRaster raster) {
++ static void copyData(final Rectangle aoi, final RenderedImage source, final WritableRaster target) {
/*
* Iterate over all tiles that interesect the area of interest. For each tile,
* copy a few rows in a temporary buffer, then copy that buffer to destination.
@@@ -487,7 -487,7 +488,7 @@@
* Note that `tb` should never be empty since we restrict iteration to the tiles
* that intersect the given area of interest.
*/
-- final TileOpExecutor executor = new TileOpExecutor(this, aoi) {
++ final TileOpExecutor executor = new TileOpExecutor(source, aoi) {
/** For copying data using data type specified by raster. */ private Object buffer;
/** For detecting if {@link #buffer} size is sufficient. */ private int bufferCapacity;
@@@ -503,12 -503,12 +504,12 @@@
while (tb.y < afterLastRow) {
final int height = Math.min(tb.height, afterLastRow - tb.y);
buffer = tile.getDataElements(tb.x, tb.y, tb.width, height, buffer);
-- raster.setDataElements(tb.x, tb.y, tb.width, height, buffer);
++ target.setDataElements(tb.x, tb.y, tb.width, height, buffer);
tb.y += height;
}
}
};
-- executor.readFrom(this);
++ executor.readFrom(source);
}
/**
@@@ -523,6 -523,6 +524,23 @@@
return null;
}
++ /**
++ * Verifies if the color model is compatible with the sample model.
++ * If the color model is incompatible, then this method returns the name of the mismatched property.
++ * If the returned property is an empty string, then the mismatched property is unidentified.
++ *
++ * @param sm the sample model. Shall not be null.
++ * @param cm the color model, or {@code null} if unspecified.
++ * @return name of mismatched property (an empty string if unidentified),
++ * or {@code null} if the color model is null or is compatible.
++ */
++ static String verifyCompatibility(final SampleModel sm, final ColorModel cm) {
++ if (cm == null || cm.isCompatibleSampleModel(sm)) return null;
++ if (cm.getNumComponents() != sm.getNumBands()) return "numComponents";
++ if (cm.getTransferType() != sm.getTransferType()) return "transferType";
++ return "";
++ }
++
/**
* Verifies whether image layout information are consistent. This method verifies that the coordinates
* of image upper-left corner are equal to the coordinates of the upper-left corner of the tile in the
@@@ -535,7 -535,7 +553,7 @@@
* <table class="sis">
* <caption>Identifiers of inconsistent values</caption>
* <tr><th>Identifier</th> <th>Meaning</th></tr>
-- * <tr><td>{@code "SampleModel"}</td> <td>Sample model is incompatible with color model.</td></tr>
++ * <tr><td>{@code "colorModel"}</td> <td>Color model is incompatible with sample model.</td></tr>
* <tr><td>{@code "tileWidth"}</td> <td>Tile width is greater than sample model width.</td></tr>
* <tr><td>{@code "tileHeight"}</td> <td>Tile height is greater than sample model height.</td></tr>
* <tr><td>{@code "numXTiles"}</td> <td>Number of tiles on the X axis is inconsistent with image width.</td></tr>
@@@ -549,6 -549,6 +567,8 @@@
* Subclasses may perform additional checks. For example, some subclasses have specialized checks
* for {@code "minX"}, {@code "minY"}, {@code "tileGridXOffset"} and {@code "tileGridYOffset"}
* values before to fallback on the more generic {@code "tileX"} and {@code "tileY"} above checks.
++ * The returned identifiers may also have subcategories. For example {@code "colorModel"} may be
++ * subdivided with {@code "colorModel.numBands"} and {@code "colorModel.transferType"}.
*
* <h4>Ignorable inconsistency</h4>
* Inconsistency in {@code "width"} and {@code "height"} values may be acceptable
@@@ -565,9 -565,9 +585,11 @@@
final int tileHeight = getTileHeight();
final SampleModel sm = getSampleModel();
if (sm != null) {
-- final ColorModel cm = getColorModel();
++ final String cm = verifyCompatibility(sm, getColorModel());
if (cm != null) {
-- if (!cm.isCompatibleSampleModel(sm)) return "colorModel";
++ String p = "colorModel";
++ if (!cm.isEmpty()) p = p + '.' + cm;
++ return p;
}
/*
* The SampleModel size represents the physical layout of pixels in the data buffer,
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourcesArgument.java
index 0000000000,0000000000..5978d0a5c3
new file mode 100644
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourcesArgument.java
@@@ -1,0 -1,0 +1,329 @@@
++/*
++ * 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.List;
++import java.util.Arrays;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.Objects;
++import java.util.function.Function;
++import java.util.function.ToIntFunction;
++import org.apache.sis.coverage.SampleDimension;
++import org.apache.sis.coverage.grid.GridGeometry;
++import org.apache.sis.coverage.grid.IllegalGridGeometryException;
++import org.apache.sis.util.resources.Errors;
++import org.apache.sis.util.ArgumentChecks;
++import org.apache.sis.util.ArraysExt;
++import org.apache.sis.util.ComparisonMode;
++
++
++/**
++ * Helper class for building a list of sample dimensions from aggregated sources.
++ * This helper class is shared for aggregation operations on different sources:
++ * rendered images, grid coverages and resources.
++ *
++ * <p>Instances of this class should be short-lived.
++ * They are used only the time needed for constructing an image or coverage operation.</p>
++ *
++ * @todo Verify if a source is itself an aggregated image or coverage,
++ * and provide a way to get a flattened view of such nested aggregations.
++ *
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ *
++ * @param <S> type of objects that are the source of sample dimensions.
++ *
++ * @since 1.4
++ */
++public final class MultiSourcesArgument<S> {
++ /**
++ * The sources of sample dimensions with empty sources removed.
++ * After a {@code validate(…)} method has been invoked, this array become a
++ * (potentially modified) copy of the array argument given to the constructor.
++ */
++ private S[] sources;
++
++ /**
++ * Indices of selected sample dimensions for each source.
++ * After a {@code validate(…)} method has been invoked, this array become a
++ * (potentially modified) copy of the array argument given to the constructor,
++ * with the same length than {@link #sources} and all elements themselves copied.
++ * The array is non-null but may contain {@code null} elements for meaning "all bands".
++ */
++ private int[][] bandsPerSource;
++
++ /**
++ * Whether to allow null elements in {@link #bandsPerSource} for meaning "all bands".
++ */
++ private boolean identityAsNull;
++
++ /**
++ * Union of all selected bands in all specified sources, or {@code null} if not applicable.
++ */
++ private List<SampleDimension> ranges;
++
++ /**
++ * Total number of bands. This is the length of the {@link #ranges} list,
++ * except that this information is provided even if {@code ranges} is null.
++ */
++ private int numBands;
++
++ /**
++ * Index of a source having the same "grid to CRS" transform than the grid geometry
++ * returned by {@link #domain(Function)}. If there is none, then this value is -1.
++ */
++ private int sourceOfGridToCRS;
++
++ /**
++ * Whether one of the {@code validate(…)} methods has been invoked.
++ */
++ private boolean validated;
++
++ /**
++ * Prepares an argument validator for the given sources and bands arguments.
++ * One of the {@code validate(…)} method should be invoked after this constructor.
++ *
++ * @param sources the sources from which to get the sample dimensions.
++ * @param bandsPerSource sample dimensions for each source. May contain {@code null} elements.
++ */
++ public MultiSourcesArgument(final S[] sources, final int[][] bandsPerSource) {
++ this.sources = sources;
++ this.bandsPerSource = bandsPerSource;
++ sourceOfGridToCRS = -1;
++ }
++
++ /**
++ * Requests the use of {@code null} elements for meaning "all bands".
++ * The null elements can appear in the {@link #bandsPerSource()} array,
++ * but the array itself will still never null.
++ */
++ public void identityAsNull() {
++ if (validated) throw new IllegalStateException();
++ identityAsNull = true;
++ }
++
++ /**
++ * Clones and validates the arguments given to the constructor.
++ *
++ * @param counter method to invoke for counting the number of bands in a source.
++ * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
++ */
++ public void validate(final ToIntFunction<S> counter) {
++ validate(null, Objects.requireNonNull(counter));
++ }
++
++ /**
++ * Clones and validates the arguments given to the constructor.
++ * Also computes the union of bands in the sources given at construction time.
++ * The union result is stored in {@link #ranges}.
++ *
++ * @param getter method to invoke for getting the list of sample dimensions.
++ * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
++ */
++ public void validate(final Function<S, List<SampleDimension>> getter) {
++ ranges = new ArrayList<>();
++ validate(Objects.requireNonNull(getter), null);
++ }
++
++ /**
++ * Computes the union of bands in the sources given at construction time.
++ * This method also verifies the indices in band arguments.
++ * Sources with no indices are removed from the iterator.
++ *
++ * <p>Exactly one of {@code getter} or {@code count} arguments shall be non-null.</p>
++ *
++ * @param getter method to invoke for getting the list of sample dimensions.
++ * @param counter method to invoke for counting the number of bands in a source.
++ * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
++ */
++ private void validate(final Function<S, List<SampleDimension>> getter, final ToIntFunction<S> counter) {
++ /*
++ * Ensure that both arrays are non-null and have the same length.
++ * Copy those arrays as their content may be overwritten.
++ */
++ ArgumentChecks.ensureNonEmpty("sources", sources);
++ final int sourceCount = sources.length;
++ if (bandsPerSource != null) {
++ if (bandsPerSource.length > sourceCount) {
++ throw new IllegalArgumentException(Errors.format(
++ Errors.Keys.TooManyCollectionElements_3,
++ "bandsPerSource", bandsPerSource.length, sourceCount));
++ }
++ bandsPerSource = Arrays.copyOf(bandsPerSource, sourceCount);
++ } else {
++ bandsPerSource = new int[sourceCount][];
++ }
++ sources = sources.clone();
++ /*
++ * Compute the number of sources and the total number of bands.
++ * This loop ensures that all band indices are in their ranges of validity
++ * with no duplicated value, then stores a copy of the band indices or null.
++ * If an empty array of bands is specified, then the source is omitted.
++ */
++ final HashMap<Integer,int[]> pool = identityAsNull ? null : new HashMap<>();
++ int filteredCount = 0;
++ for (int i=0; i<sourceCount; i++) {
++ int[] selected = bandsPerSource[i];
++ if (selected != null && selected.length == 0) {
++ // Note that the source is allowed to be null in this particular case.
++ continue;
++ }
++ final S source = sources[i];
++ sources[filteredCount] = source;
++ ArgumentChecks.ensureNonNullElement("sources", i, source);
++ /*
++ * Get the number of bands, or optionally the bands themselves.
++ * This information is required before to validate arguments.
++ */
++ final List<SampleDimension> bands;
++ final int n;
++ if (getter != null) {
++ bands = getter.apply(source);
++ n = bands.size();
++ } else {
++ bands = null;
++ n = counter.applyAsInt(source);
++ }
++ /*
++ * If the next source is the same than the source in current iteration, merge the bands together.
++ * The `BandAggregateGridResource.read(…)` implementation relies on that optimization.
++ */
++ final int next = i+1;
++ if (next < sourceCount && sources[next] == source) {
++ final int[] nextBands = bandsPerSource[next];
++ ArgumentChecks.ensureNonNullElement("bandsPerSource", i, selected);
++ ArgumentChecks.ensureNonNullElement("bandsPerSource", next, nextBands);
++ final int[] merged = Arrays.copyOf(selected, selected.length + nextBands.length);
++ System.arraycopy(nextBands, 0, merged, selected.length, nextBands.length);
++ bandsPerSource[next] = merged;
++ bandsPerSource[i] = ArraysExt.EMPTY_INT;
++ continue;
++ }
++ /*
++ * Validate the `bandsPerSource` argument given at construction time.
++ * Then store a copy of that argument.
++ */
++ final var range = RangeArgument.validate(n, selected, null);
++ if (range.isIdentity()) {
++ selected = (pool != null) ? pool.computeIfAbsent(n, (k) -> ArraysExt.range(0, k)) : null;
++ if (ranges != null) {
++ ranges.addAll(bands);
++ }
++ } else {
++ selected = range.getSelectedBands();
++ if (ranges != null) {
++ for (int b : selected) {
++ ranges.add(bands.get(b));
++ }
++ }
++ }
++ bandsPerSource[filteredCount++] = selected;
++ numBands += range.getNumBands();
++ }
++ sources = ArraysExt.resize(sources, filteredCount);
++ bandsPerSource = ArraysExt.resize(bandsPerSource, filteredCount);
++ validated = true;
++ }
++
++ /**
++ * Returns {@code true} if there is only one source with all bands selected.
++ *
++ * @return whether {@code sources[0]} could be used directly.
++ */
++ public boolean isIdentity() {
++ return bandsPerSource.length == 1 && bandsPerSource[0] == null;
++ }
++
++ /**
++ * Returns all sources coverages as a (potentially modified)
++ * copy of the array argument given to the constructor.
++ *
++ * @return all validated sources.
++ */
++ @SuppressWarnings("ReturnOfCollectionOrArrayField")
++ public S[] sources() {
++ if (validated) return sources;
++ throw new IllegalStateException();
++ }
++
++ /**
++ * Computes the intersection of the grid geometries of all sources.
++ * This method also verifies that all grid geometries are compatible.
++ *
++ * @param getter the method to invoke for getting grid geometry from a source.
++ * @return intersection of all grid geometries.
++ * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
++ *
++ * @todo Current implementation requires that all grid geometry are equal. We need to relax that.
++ */
++ public GridGeometry domain(final Function<S, GridGeometry> getter) {
++ GridGeometry intersection = getter.apply(sources[0]);
++ for (int i=1; i < sources.length; i++) {
++ if (!intersection.equals(getter.apply(sources[i]), ComparisonMode.IGNORE_METADATA)) {
++ throw new IllegalGridGeometryException("Not yet supported on coverages with different grid geometries.");
++ }
++ }
++ sourceOfGridToCRS = 0; // TODO: to be computed when different grid geometries will be allowed. Prefer widest extent.
++ return intersection;
++ }
++
++ /**
++ * Returns the union of all selected bands in all specified sources.
++ * The returned list is modifiable.
++ *
++ * @return all selected sample dimensions.
++ */
++ @SuppressWarnings("ReturnOfCollectionOrArrayField")
++ public List<SampleDimension> ranges() {
++ if (ranges != null) return ranges;
++ throw new IllegalStateException();
++ }
++
++ /**
++ * Returns the total number of bands.
++ *
++ * @return total number of bands.
++ */
++ public int numBands() {
++ if (validated) return numBands;
++ throw new IllegalStateException();
++ }
++
++ /**
++ * Returns the indices of selected bands as (potentially modified)
++ * copies of the arrays argument given to the constructor.
++ *
++ * @return indices of selected sample dimensions for each source.
++ * Never null but may contain null elements if {@link #identityAsNull()} has been invoked.
++ */
++ @SuppressWarnings("ReturnOfCollectionOrArrayField")
++ public int[][] bandsPerSource() {
++ if (validated) return bandsPerSource;
++ throw new IllegalStateException();
++ }
++
++ /**
++ * Returns the index of a source having the same "grid to CRS" transform than the grid geometry
++ * returned by {@link #domain(Function)}.
++ *
++ * @return index of a sources having the same "grid to CRS" than the domain, or -1 if none.
++ */
++ public int sourceOfGridToCRS() {
++ return sourceOfGridToCRS;
++ }
++}
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/coverage/RangeArgument.java
index f0ac4ed796,f0ac4ed796..ee72fbac4b
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/RangeArgument.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/RangeArgument.java
@@@ -14,7 -14,7 +14,7 @@@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
--package org.apache.sis.internal.storage;
++package org.apache.sis.internal.coverage;
import java.util.List;
import java.util.Arrays;
@@@ -27,7 -27,7 +27,7 @@@ import org.apache.sis.coverage.SampleDi
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
import org.apache.sis.internal.coverage.j2d.SampleModelFactory;
--import org.apache.sis.storage.GridCoverageResource;
++import org.apache.sis.internal.feature.Resources;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
@@@ -38,7 -38,7 +38,7 @@@ import org.apache.sis.util.Localized
* The user-provided {@code ranges} argument together with a set of convenience tools.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 0.8
*/
public final class RangeArgument {
@@@ -80,14 -80,14 +80,15 @@@
}
/**
-- * Validate the {@code ranges} argument given to {@link GridCoverageResource#read(GridGeometry, int...)}.
-- * This method verifies that all indices are between 0 and {@code numSampleDimensions} and that there is
-- * no duplicated index.
++ * Validate the {@code ranges} argument given to
++ * {@link org.apache.sis.storage.GridCoverageResource#read(GridGeometry, int...)}.
++ * This method verifies that all indices are between 0 and {@code numSampleDimensions}
++ * and that there is no duplicated index.
*
* @param numSampleDimensions number of sample dimensions in the resource.
-- * Equal to <code>{@linkplain GridCoverageResource#getSampleDimensions()}.size()</code>.
++ * Equal to <code>{@linkplain org.apache.sis.storage.GridCoverageResource#getSampleDimensions()}.size()</code>.
* @param ranges the {@code ranges} argument given by the user. May be null or empty.
-- * @param listeners source of locale to use if an exception must be thrown.
++ * @param listeners source of locale to use if an exception must be thrown, or {@code null} if none.
* @return the {@code ranges} argument encapsulated with a set of convenience tools.
* @throws IllegalArgumentException if a range index is invalid.
*/
@@@ -107,7 -107,7 +108,7 @@@
for (int i=0; i<ranges.length; i++) {
final int r = ranges[i];
if (r < 0 || r >= numSampleDimensions) {
-- throw new IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString(
++ throw new IllegalArgumentException(resources(listeners).getString(
Resources.Keys.InvalidSampleDimensionIndex_2, numSampleDimensions - 1, r));
}
packed[i] = (((long) r) << Integer.SIZE) | i;
@@@ -122,7 -122,7 +123,7 @@@
// Never negative because of check in previous loop.
final int r = (int) (packed[i] >>> Integer.SIZE);
if (r == previous) {
-- throw new IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString(
++ throw new IllegalArgumentException(resources(listeners).getString(
Resources.Keys.DuplicatedSampleDimensionIndex_1, r));
}
previous = r;
@@@ -131,6 -131,6 +132,16 @@@
return new RangeArgument(packed, packed.length == numSampleDimensions);
}
++ /**
++ * Returns the resources for the locale specified by the given listeners.
++ *
++ * @param listeners source of locale to use for an exception to be thrown, or {@code null} if none.
++ * @return the resources for the given locale.
++ */
++ private static Resources resources(final Localized listeners) {
++ return Resources.forLocale((listeners != null) ? listeners.getLocale() : null);
++ }
++
/**
* Returns {@code true} if user specified all bands in increasing order.
* This method always return {@code false} if {@link #insertSubsampling(int[], int)} has been invoked.
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
index de0d86719d,de0d86719d..3a942df66d
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
@@@ -527,17 -527,17 +527,20 @@@ public final class ColorModelFactory
* The sample model shall use integer type and have 3 or 4 bands.
*
* @param model the sample model for which to create a color model.
-- * @return the color model.
++ * @return the color model, or empty if a precondition does not hold.
*/
-- public static ColorModel createRGB(final SampleModel model) {
++ public static Optional<ColorModel> createRGB(final SampleModel model) {
final int numBands = model.getNumBands();
-- assert numBands >= 3 && numBands <= 4 : numBands;
-- assert ImageUtilities.isIntegerType(model) : model;
-- int bitsPerSample = 0;
-- for (int i=0; i<numBands; i++) {
-- bitsPerSample = Math.max(bitsPerSample, model.getSampleSize(i));
++ if (numBands >= 3 && numBands <= 4 && ImageUtilities.isIntegerType(model)) {
++ int bitsPerSample = 0;
++ for (int i=0; i<numBands; i++) {
++ bitsPerSample = Math.max(bitsPerSample, model.getSampleSize(i));
++ }
++ if (bitsPerSample <= Byte.SIZE) {
++ return Optional.of(createRGB(bitsPerSample, model.getNumDataElements() == 1, numBands > 3));
++ }
}
-- return createRGB(bitsPerSample, model.getNumDataElements() == 1, numBands > 3);
++ return Optional.empty();
}
/**
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
index 1b3bd172ee,1b3bd172ee..a985bc2251
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
@@@ -38,7 -38,7 +38,7 @@@ import org.apache.sis.internal.system.C
* those information directly, but provides method for deriving those properties from a given image.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.1
*/
public class ImageLayout {
@@@ -71,7 -71,7 +71,7 @@@
*
* @see ImageUtilities#DEFAULT_TILE_SIZE
*/
-- private final int preferredTileWidth, preferredTileHeight;
++ protected final int preferredTileWidth, preferredTileHeight;
/**
* Whether image size can be modified if needed. Changes are applied only if an image cannot be tiled
@@@ -92,8 -92,8 +92,8 @@@
*/
public ImageLayout(final Dimension preferredTileSize, final boolean isBoundsAdjustmentAllowed) {
if (preferredTileSize != null) {
-- preferredTileWidth = preferredTileSize.width;
-- preferredTileHeight = preferredTileSize.height;
++ preferredTileWidth = Math.max(1, preferredTileSize.width);
++ preferredTileHeight = Math.max(1, preferredTileSize.height);
} else {
preferredTileWidth = ImageUtilities.DEFAULT_TILE_SIZE;
preferredTileHeight = ImageUtilities.DEFAULT_TILE_SIZE;
@@@ -319,20 -319,20 +319,22 @@@
}
/**
-- * Creates a banded sample model of the given type with
-- * {@linkplain #suggestTileSize(RenderedImage, Rectangle, boolean) the suggested tile size} for the given image.
++ * Creates a banded sample model of the given type with an automatic tile size.
++ * At least one of {@code image} and {@code bounds} arguments must be non null.
++ * This method uses the {@linkplain #suggestTileSize(RenderedImage, Rectangle, boolean)
++ * suggested tile size} for the given image and bounds.
*
-- * @param type desired data type as a {@link java.awt.image.DataBuffer} constant.
++ * @param dataType desired data type as a {@link java.awt.image.DataBuffer} constant.
* @param numBands desired number of bands.
* @param image the image which will be the source of the image for which a sample model is created.
* @param bounds the bounds of the image to create, or {@code null} if same as {@code image}.
* @return a banded sample model of the given type with the given number of bands.
*/
-- public BandedSampleModel createBandedSampleModel(final int type, final int numBands,
++ public BandedSampleModel createBandedSampleModel(final int dataType, final int numBands,
final RenderedImage image, final Rectangle bounds)
{
-- final Dimension tile = suggestTileSize(image, bounds, isBoundsAdjustmentAllowed);
-- return RasterFactory.unique(new BandedSampleModel(type, tile.width, tile.height, numBands));
++ final Dimension tileSize = suggestTileSize(image, bounds, isBoundsAdjustmentAllowed);
++ return RasterFactory.unique(new BandedSampleModel(dataType, tileSize.width, tileSize.height, numBands));
}
/**
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index e79fd39687,e79fd39687..8027050fb0
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@@ -148,8 -148,8 +148,8 @@@ public final class ImageUtilities exten
/**
* If the given image is showing only one band, returns the index of that band.
-- * Otherwise returns 0. Image showing only one band are SIS-specific (usually an
-- * image show all its bands).
++ * Otherwise returns -1. Image showing only one band are SIS-specific
++ * (usually an image shows all its bands).
*
* @param image the image for which to get the visible band, or {@code null}.
* @return index of the visible band, or -1 if there is none or more than one.
@@@ -416,7 -416,7 +416,7 @@@
}
/**
-- * Returns {@code true} if the given sample model use an integer type.
++ * Returns {@code true} if the given sample model uses an integer type.
* Returns {@code false} if the type is a floating point type or in case
* of doubt (e.g. for {@link DataBuffer#TYPE_UNDEFINED}).
*
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/coverage/package-info.java
index 6c075d95e7,6c075d95e7..52ce228557
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/package-info.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/package-info.java
@@@ -24,7 -24,7 +24,7 @@@
* may change in incompatible ways in any future version without notice.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.1
*/
package org.apache.sis.internal.coverage;
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
index d15501c53e,d15501c53e..caf2a3ec8e
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
@@@ -62,6 -62,6 +62,11 @@@ public final class Resources extends In
*/
public static final short AbstractFeatureType_1 = 1;
++ /**
++ * Omission of the “{0}” grid axes would create ambiguity.
++ */
++ public static final short AmbiguousGridAxisOmission_1 = 84;
++
/**
* Cannot assign characteristics to the “{0}” property.
*/
@@@ -157,6 -157,6 +162,11 @@@
*/
public static final short DependencyNotFound_3 = 18;
++ /**
++ * Sample dimension index {0} is duplicated.
++ */
++ public static final short DuplicatedSampleDimensionIndex_1 = 85;
++
/**
* Image has zero pixel.
*/
@@@ -241,6 -241,6 +251,11 @@@
*/
public static final short ImageIsOpaque = 34;
++ /**
++ * Color model is incompatible with sample model.
++ */
++ public static final short IncompatibleColorModel = 82;
++
/**
* The ({0}, {1}) tile has an unexpected size, number of bands or sample layout.
*/
@@@ -252,6 -252,6 +267,11 @@@
*/
public static final short InsufficientBufferCapacity_3 = 36;
++ /**
++ * Sample dimension index {1} is invalid. Expected an index from 0 to {0} inclusive.
++ */
++ public static final short InvalidSampleDimensionIndex_2 = 86;
++
/**
* Iteration is finished.
*/
@@@ -326,6 -326,6 +346,11 @@@
*/
public static final short NonLinearInDimensions_1 = 51;
++ /**
++ * The dimensions to reduce cannot be separated.
++ */
++ public static final short NonSeparableReducedDimensions = 83;
++
/**
* Value provided by first expression is not a geometry.
*/
@@@ -372,6 -372,6 +397,11 @@@
*/
public static final short PropertyNotFound_2 = 59;
++ /**
++ * Source images do not intersect.
++ */
++ public static final short SourceImagesDoNotIntersect = 80;
++
/**
* Tile ({0}, {1}) is unavailable because of error in a previous calculation attempt.
*/
@@@ -433,6 -433,6 +463,11 @@@
*/
public static final short UnresolvedFeatureName_1 = 71;
++ /**
++ * No bands have been specified.
++ */
++ public static final short UnspecifiedBands = 81;
++
/**
* Coordinate reference system is unspecified.
*/
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
index 3c0eb6a938,3c0eb6a938..0a1c6c2018
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
@@@ -20,6 -20,6 +20,7 @@@
# For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package.
#
AbstractFeatureType_1 = Feature type \u2018{0}\u2019 is abstract.
++AmbiguousGridAxisOmission_1 = Omission of the \u201c{0}\u201d grid axes would create ambiguity.
CanNotAssignCharacteristics_1 = Cannot assign characteristics to the \u201c{0}\u201d property.
CanNotBuildGridCoverage = Cannot build the grid coverage.
CanNotComputeTile_2 = Cannot compute tile ({0}, {1}).
@@@ -39,6 -39,6 +40,7 @@@ CharacteristicsAlreadyExists_2 = Cha
CharacteristicsNotFound_2 = No characteristics named \u201c{1}\u201d has been found in \u201c{0}\u201d attribute.
CrsToGridConversion = Conversions from coverage CRS to grid cell indices.
DependencyNotFound_3 = Operation \u201c{0}\u201d requires a \u201c{1}\u201d property, but no such property has been found in \u201c{2}\u201d.
++DuplicatedSampleDimensionIndex_1 = Sample dimension index {0} is duplicated.
EmptyImage = Image has zero pixel.
EmptyTileOrImageRegion = Empty tile or image region.
GridCoordinateOutsideCoverage_4 = Indices ({3}) are outside grid coverage. The value in dimension {0} shall be between {1,number} and {2,number} inclusive.
@@@ -55,10 -55,10 +57,12 @@@ IllegalTransferFunction_1 = Ill
ImageAllowsTransparency = Image allows transparency.
ImageHasAlphaChannel = Image has alpha channel.
ImageIsOpaque = Image is opaque.
++IncompatibleColorModel = Color model is incompatible with sample model.
IncompatibleTile_2 = The ({0}, {1}) tile has an unexpected size, number of bands or sample layout.
InsufficientBufferCapacity_3 = Data buffer capacity is insufficient for a grid of {0} cells \u00d7 {1} bands. Missing {2} elements.
IterationIsFinished = Iteration is finished.
IterationNotStarted = Iteration did not started.
++InvalidSampleDimensionIndex_2 = Sample dimension index {1} is invalid. Expected an index from 0 to {0} inclusive.
MismatchedBandCount_2 = Image number of bands {0,number} does not match the number of sample dimensions ({1,number}).
MismatchedBandSize = The bands have different number of sample values.
MismatchedDataType = The bands store sample values using different data types.
@@@ -71,6 -71,6 +75,7 @@@ MismatchedValueClass_3 = An
NoCategoryForValue_1 = No category for value {0}.
NoNDimensionalSlice_3 = Cannot infer a {0}-dimensional slice from the grid envelope. Dimension {1} has {2,number} cells.
NonLinearInDimensions_1 = non-linear in {0} dimension{0,choice,1#|2#s}:
++NonSeparableReducedDimensions = The dimensions to reduce cannot be separated.
NotAGeometryAtFirstExpression = Value provided by first expression is not a geometry.
NotASingleton_1 = Property \u201c{0}\u201d contains more than one value.
NotStrictlyOrderedDimensions = The specified dimensions are not in strictly ascending order.
@@@ -80,6 -80,6 +85,7 @@@ OutOfIteratorDomain_2 = Th
PointOutsideCoverageDomain_1 = Point ({0}) is outside the coverage domain.
PropertyAlreadyExists_2 = Property \u201c{1}\u201d already exists in feature \u201c{0}\u201d.
PropertyNotFound_2 = No property named \u201c{1}\u201d has been found in \u201c{0}\u201d feature.
++SourceImagesDoNotIntersect = Source images do not intersect.
TileErrorFlagSet_2 = Tile ({0}, {1}) is unavailable because of error in a previous calculation attempt.
TileNotWritable_2 = Tile ({0}, {1}) is not writable.
TooManyQualitatives = Too many qualitative categories.
@@@ -92,6 -92,6 +98,7 @@@ UnexpectedNumberOfComponents_4 = Th
UnknownDataType_1 = Raster data type \u2018{0}\u2019 is unknown or unsupported.
UnknownFunction_1 = Function \u201c{0}\u201d is unknown or unsupported.
UnresolvedFeatureName_1 = Feature named \u201c{0}\u201d has not yet been resolved.
++UnspecifiedBands = No bands have been specified.
UnspecifiedCRS = Coordinate reference system is unspecified.
UnspecifiedGridExtent = Grid extent is unspecified.
UnspecifiedRasterData = Raster data are unspecified.
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
index 0ce750b962,0ce750b962..356dc1cecf
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
@@@ -25,6 -25,6 +25,7 @@@
# U+00A0 NO-BREAK SPACE before :
#
AbstractFeatureType_1 = Le type d\u2019entit\u00e9 \u2018{0}\u2019 est abstrait.
++AmbiguousGridAxisOmission_1 = L\u2019omission de l\u2019axe \u201c{0}\u201d de la grille cr\u00e9e une ambigu\u00eft\u00e9.
CanNotAssignCharacteristics_1 = Ne peut pas assigner des caract\u00e9ristiques \u00e0 la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb.
CanNotBuildGridCoverage = Ne peut pas construire la couverture de donn\u00e9es.
CanNotComputeTile_2 = Ne peut pas calculer la tuile ({0}, {1}).
@@@ -44,6 -44,6 +45,7 @@@ CharacteristicsAlreadyExists_2 = La
CharacteristicsNotFound_2 = Aucune caract\u00e9ristique nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb n\u2019a \u00e9t\u00e9 trouv\u00e9e dans l\u2019attribut \u00ab\u202f{0}\u202f\u00bb.
CrsToGridConversion = Conversions des coordonn\u00e9es de la couverture de donn\u00e9es vers les indices des cellules de la grille.
DependencyNotFound_3 = L\u2019op\u00e9ration \u00ab\u202f{0}\u202f\u00bb n\u00e9cessite une propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb, mais cette propri\u00e9t\u00e9 n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans \u00ab\u202f{2}\u202f\u00bb.
++DuplicatedSampleDimensionIndex_1 = L\u2019index de dimension d\u2019\u00e9chantillonnage {0} est r\u00e9p\u00e9t\u00e9.
EmptyImage = L\u2019image a z\u00e9ro pixel.
EmptyTileOrImageRegion = La tuile ou la r\u00e9gion de l\u2019image est vide.
GridCoordinateOutsideCoverage_4 = Les indices ({3}) sont en dehors du domaine de la grille. La valeur dans la dimension {0} doit \u00eatre entre {1,number} et {2,number} inclusivement.
@@@ -60,10 -60,10 +62,12 @@@ IllegalTransferFunction_1 = Fon
ImageAllowsTransparency = L\u2019image permet la transparence.
ImageHasAlphaChannel = L\u2019image a un canal alpha.
ImageIsOpaque = L\u2019image est opaque.
++IncompatibleColorModel = Le mod\u00e8le de couleurs est incompatible.
IncompatibleTile_2 = La tuile ({0}, {1}) a une taille, un nombre de bandes ou une disposition des valeurs inattendu.
InsufficientBufferCapacity_3 = La capacit\u00e9 du buffer est insuffisante pour une grille de {0} cellules \u00d7 {1} bandes. Il lui manque {2} \u00e9l\u00e9ments.
IterationIsFinished = L\u2019it\u00e9ration est termin\u00e9e.
IterationNotStarted = L\u2019it\u00e9ration n\u2019a pas commenc\u00e9e.
++InvalidSampleDimensionIndex_2 = L\u2019index de dimension d\u2019\u00e9chantillonnage {1} est invalide. On attendait un index de 0 \u00e0 {0} inclusif.
MismatchedBandCount_2 = Le nombre de bandes de l\u2019image ({0,number}) ne correspond pas au nombre de dimensions d\u2019\u00e9chantillonnage ({1,number}).
MismatchedBandSize = Les bandes ont un nombre diff\u00e9rent de valeurs.
MismatchedDataType = Les bandes stockent leurs valeurs en utilisant des types de donn\u00e9es diff\u00e9rents.
@@@ -76,6 -76,6 +80,7 @@@ MismatchedValueClass_3 = Un
NoCategoryForValue_1 = Aucune cat\u00e9gorie n\u2019est d\u00e9finie pour la valeur {0}.
NoNDimensionalSlice_3 = Ne peut pas inf\u00e9rer une tranche \u00e0 {0} dimensions \u00e0 partir de l\u2019enveloppe de la grille. La dimension {1} a {2,number} cellules.
NonLinearInDimensions_1 = non-lin\u00e9aire dans {0} dimension{0,choice,1#|2#s}\u00a0:
++NonSeparableReducedDimensions = Les dimensions \u00e0 r\u00e9duire ne peuvent pas \u00eatre s\u00e9par\u00e9es.
NotAGeometryAtFirstExpression = La valeur fournie par la premi\u00e8re expression n\u2019est pas une g\u00e9om\u00e9trie.
NotASingleton_1 = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb contient plus de une valeur.
NotStrictlyOrderedDimensions = Les dimensions sp\u00e9cifi\u00e9es ne sont pas en ordre strictement croissant.
@@@ -85,6 -85,6 +90,7 @@@ OutOfIteratorDomain_2 = La
PointOutsideCoverageDomain_1 = Le point ({0}) est en dehors du domaine de la couverture de donn\u00e9es.
PropertyAlreadyExists_2 = La propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb existe d\u00e9j\u00e0 dans l\u2019entit\u00e9 \u00ab\u202f{0}\u202f\u00bb.
PropertyNotFound_2 = Aucune propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb n\u2019a \u00e9t\u00e9 trouv\u00e9e dans l\u2019entit\u00e9 \u00ab\u202f{0}\u202f\u00bb.
++SourceImagesDoNotIntersect = Des images sources ne s\u2019intersectent pas.
TileErrorFlagSet_2 = La tuile ({0}, {1}) est indisponible pour cause d\u2019erreur lors d\u2019une tentative ant\u00e9rieure de calcul.
TileNotWritable_2 = La tuile ({0}, {1}) n\u2019est pas d\u00e9clar\u00e9e en \u00e9criture.
TooManyQualitatives = Trop de cat\u00e9gories qualitatives.
@@@ -97,6 -97,6 +103,7 @@@ UnexpectedNumberOfComponents_4 = La
UnknownDataType_1 = Le type de donn\u00e9es raster \u2018{0}\u2019 est inconnu ou non-support\u00e9.
UnknownFunction_1 = La fonction \u00ab\u202f{0}\u202f\u00bb est inconnue ou non-support\u00e9e.
UnresolvedFeatureName_1 = L\u2019entit\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb n\u2019a pas encore \u00e9t\u00e9 r\u00e9solue.
++UnspecifiedBands = Aucune bande n\u2019a \u00e9t\u00e9 sp\u00e9cifi\u00e9e.
UnspecifiedCRS = Le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es n\u2019a pas \u00e9t\u00e9 sp\u00e9cifi\u00e9.
UnspecifiedGridExtent = L\u2019\u00e9tendue de la grille n\u2019a pas \u00e9t\u00e9 sp\u00e9cifi\u00e9e.
UnspecifiedRasterData = Les donn\u00e9es du raster n\u2019ont pas \u00e9t\u00e9 sp\u00e9cifi\u00e9es.
diff --cc core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
index 0000000000,0000000000..565bc0af2d
new file mode 100644
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
@@@ -1,0 -1,0 +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.*;
++
++
++/**
++ * 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-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
index 0000000000,868113a33d..5af127340f
mode 000000,100644..100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
@@@ -1,0 -1,226 +1,308 @@@
++/*
++ * 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.image;
+
+ import java.awt.Rectangle;
+ import java.awt.image.BufferedImage;
+ import java.awt.image.DataBuffer;
+ import java.awt.image.Raster;
+ import java.awt.image.RenderedImage;
-import java.awt.image.WritableRenderedImage;
-import java.util.Arrays;
-import java.util.function.IntBinaryOperator;
+ import java.util.stream.IntStream;
++import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.test.TestCase;
++import org.apache.sis.test.DependsOnMethod;
+ import org.junit.Test;
+
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
++import static org.junit.Assert.*;
+
-public class BandAggregateImageTest extends TestCase {
+
++/**
++ * Tests {@link BandAggregateImage}.
++ *
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ * @since 1.4
++ */
++public final class BandAggregateImageTest extends TestCase {
++ /**
++ * The image processor to use for testing band aggregations.
++ */
++ private final ImageProcessor processor;
++
++ /**
++ * Creates a new test case.
++ */
++ public BandAggregateImageTest() {
++ processor = new ImageProcessor();
++ }
++
++ /**
++ * Tests the aggregation of two untiled images having the same bounds and only one band.
++ * This is the simplest case in this test class.
++ */
+ @Test
- public void aggregateSingleBandImages() {
- BufferedImage im1 = new BufferedImage(3, 3, BufferedImage.TYPE_BYTE_GRAY);
- im1.getRaster().setSamples(0, 0, 3, 3, 0, IntStream.range(0, 3*3).map(it -> 1).toArray());
- BufferedImage im2 = new BufferedImage(3, 3, BufferedImage.TYPE_BYTE_GRAY);
- im2.getRaster().setSamples(0, 0, 3, 3, 0, IntStream.range(0, 3*3).map(it -> 2).toArray());
++ public void aggregateUntiledImages() {
++ final int width = 3;
++ final int height = 4;
++ final BufferedImage im1 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
++ final BufferedImage im2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
++ im1.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s + 1).toArray());
++ im2.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s * 2).toArray());
+
- final RenderedImage result = processor().aggregateBands(im1, im2);
++ final RenderedImage result = processor.aggregateBands(im1, im2);
+ assertNotNull(result);
++ assertEquals(0, result.getMinTileX());
++ assertEquals(0, result.getMinTileY());
++ assertEquals(1, result.getNumXTiles());
++ assertEquals(1, result.getNumYTiles());
+
+ final Raster tile = result.getTile(0, 0);
+ assertEquals(2, tile.getNumBands());
- assertEquals(new Rectangle(0, 0, 3, 3), tile.getBounds());
++ assertEquals(new Rectangle(0, 0, width, height), tile.getBounds());
+ assertArrayEquals(
- new int[] {
- 1, 2, 1, 2, 1, 2,
-
- 1, 2, 1, 2, 1, 2,
-
- 1, 2, 1, 2, 1, 2
- },
- tile.getPixels(0, 0, 3, 3, (int[]) null)
++ new int[] {
++ 1, 0, 2, 2, 3, 4,
++ 4, 6, 5, 8, 6, 10,
++ 7, 12, 8, 14, 9, 16,
++ 10, 18, 11, 20, 12, 22
++ },
++ tile.getPixels(0, 0, width, height, (int[]) null)
+ );
+ }
+
++ /**
++ * Tests the aggregation of two tiled images having the same tile matrix.
++ * The same test is executed many times with different but equivalent classes of sample models.
++ */
+ @Test
- public void aggregateSimilarTiledImages() {
- aggregateSimilarTiledImages(true, true);
- aggregateSimilarTiledImages(false, false);
- aggregateSimilarTiledImages(true, false);
- aggregateSimilarTiledImages(false, true);
++ @DependsOnMethod("aggregateUntiledImages")
++ public void aggregateSimilarlyTiledImages() {
++ aggregateSimilarlyTiledImages(true, true);
++ aggregateSimilarlyTiledImages(false, false);
++ aggregateSimilarlyTiledImages(true, false);
++ aggregateSimilarlyTiledImages(false, true);
+ }
-
- @Test
- public void aggregateImagesUsingSameExtentButDifferentTileSizes() {
- // Note: we use different tile indices to test robustness. The aggregation algorithm should rely on pixel coordinates for absolute positioning and synchronisation of image domains.
- final TiledImageMock tiled2x2 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 3, 1, 8, 4, 2, 2, 1, 2, true);
- final TiledImageMock tiled4x1 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 3, 1, 8, 4, 4, 1, 3, 4, true);
- final TiledImageMock oneTile = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 3, 1, 8, 4, 8, 4, 5, 6, true);
-
- init(tiled2x2, tiled4x1, oneTile);
-
- final RenderedImage result = processor().aggregateBands(tiled2x2, tiled4x1, oneTile);
++
++ /**
++ * Implementation of {@link #aggregateSimilarlyTiledImages()} with sample model classes
++ * specified by the boolean arguments.
++ *
++ * @param firstBanded whether to use {@code BandedSampleModel} for the first image.
++ * @param secondBanded whether to use {@code BandedSampleModel} for the second image.
++ */
++ private void aggregateSimilarlyTiledImages(final boolean firstBanded, final boolean secondBanded) {
++ final int minX = 7;
++ final int minY = -5;
++ final int width = 6;
++ final int height = 9;
++ final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 1, 2, firstBanded);
++ final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 3, 4, secondBanded);
++ initializeAllTiles(im1, im2);
++
++ RenderedImage result = processor.aggregateBands(im1, im2);
+ assertNotNull(result);
- assertArrayEquals(new int[] { 3, 1, 8, 4, 2, 1, 1, 4 }, new int[] {
- result.getMinX(), result.getMinY(), result.getWidth(), result.getHeight(), result.getTileWidth(), result.getTileHeight(), result.getMinTileX(), result.getMinTileY()
- });
++ assertEquals(minX, result.getMinX());
++ assertEquals(minY, result.getMinY());
++ assertEquals(width, result.getWidth());
++ assertEquals(height, result.getHeight());
++ assertEquals(3, result.getTileWidth());
++ assertEquals(3, result.getTileHeight());
++ assertEquals(1, result.getMinTileX());
++ assertEquals(2, result.getMinTileY());
++ assertEquals(2, result.getNumXTiles());
++ assertEquals(3, result.getNumYTiles());
++ assertEquals(4, result.getSampleModel().getNumBands());
+
- final Raster raster = result.getData();
- assertEquals(new Rectangle(3, 1, 8, 4), raster.getBounds());
++ Raster raster = result.getData();
++ assertEquals(4, raster.getNumBands());
++ assertEquals(new Rectangle(minX, minY, width, height), raster.getBounds());
+ assertArrayEquals(
- new int[] {
- 1100, 2100, 3100, 1101, 2101, 3101, 1200, 2102, 3102, 1201, 2103, 3103, 1300, 2200, 3104, 1301, 2201, 3105, 1400, 2202, 3106, 1401, 2203, 3107,
- 1110, 2300, 3110, 1111, 2301, 3111, 1210, 2302, 3112, 1211, 2303, 3113, 1310, 2400, 3114, 1311, 2401, 3115, 1410, 2402, 3116, 1411, 2403, 3117,
- 1500, 2500, 3120, 1501, 2501, 3121, 1600, 2502, 3122, 1601, 2503, 3123, 1700, 2600, 3124, 1701, 2601, 3125, 1800, 2602, 3126, 1801, 2603, 3127,
- 1510, 2700, 3130, 1511, 2701, 3131, 1610, 2702, 3132, 1611, 2703, 3133, 1710, 2800, 3134, 1711, 2801, 3135, 1810, 2802, 3136, 1811, 2803, 3137,
- },
- raster.getPixels(3, 1, 8, 4, (int[]) null)
++ new int[] {
++ // Tile 1 Tile 2
++ 1100, 2100, 3100, 4100, 1101, 2101, 3101, 4101, 1102, 2102, 3102, 4102, 1200, 2200, 3200, 4200, 1201, 2201, 3201, 4201, 1202, 2202, 3202, 4202,
++ 1110, 2110, 3110, 4110, 1111, 2111, 3111, 4111, 1112, 2112, 3112, 4112, 1210, 2210, 3210, 4210, 1211, 2211, 3211, 4211, 1212, 2212, 3212, 4212,
++ 1120, 2120, 3120, 4120, 1121, 2121, 3121, 4121, 1122, 2122, 3122, 4122, 1220, 2220, 3220, 4220, 1221, 2221, 3221, 4221, 1222, 2222, 3222, 4222,
++ // Tile 3 Tile 4
++ 1300, 2300, 3300, 4300, 1301, 2301, 3301, 4301, 1302, 2302, 3302, 4302, 1400, 2400, 3400, 4400, 1401, 2401, 3401, 4401, 1402, 2402, 3402, 4402,
++ 1310, 2310, 3310, 4310, 1311, 2311, 3311, 4311, 1312, 2312, 3312, 4312, 1410, 2410, 3410, 4410, 1411, 2411, 3411, 4411, 1412, 2412, 3412, 4412,
++ 1320, 2320, 3320, 4320, 1321, 2321, 3321, 4321, 1322, 2322, 3322, 4322, 1420, 2420, 3420, 4420, 1421, 2421, 3421, 4421, 1422, 2422, 3422, 4422,
++ // Tile 5 Tile 6
++ 1500, 2500, 3500, 4500, 1501, 2501, 3501, 4501, 1502, 2502, 3502, 4502, 1600, 2600, 3600, 4600, 1601, 2601, 3601, 4601, 1602, 2602, 3602, 4602,
++ 1510, 2510, 3510, 4510, 1511, 2511, 3511, 4511, 1512, 2512, 3512, 4512, 1610, 2610, 3610, 4610, 1611, 2611, 3611, 4611, 1612, 2612, 3612, 4612,
++ 1520, 2520, 3520, 4520, 1521, 2521, 3521, 4521, 1522, 2522, 3522, 4522, 1620, 2620, 3620, 4620, 1621, 2621, 3621, 4621, 1622, 2622, 3622, 4622
++ },
++ raster.getPixels(minX, minY, width, height, (int[]) null)
++ );
++ /*
++ * Repeat the test with a custom band selection.
++ * One of the source images is used twice, but with a different selection of bands.
++ */
++ result = processor.aggregateBands(new RenderedImage[] {im1, im2, im1}, new int[][] {
++ new int[] {1}, // Take second band of image 1.
++ null, // Take all bands of image 2.
++ new int[] {0} // Take first band of image 1.
++ }, null);
++ assertNotNull(result);
++ assertEquals(minX, result.getMinX());
++ assertEquals(minY, result.getMinY());
++ assertEquals(width, result.getWidth());
++ assertEquals(height, result.getHeight());
++ assertEquals(3, result.getTileWidth());
++ assertEquals(3, result.getTileHeight());
++ assertEquals(1, result.getMinTileX());
++ assertEquals(2, result.getMinTileY());
++ assertEquals(2, result.getNumXTiles());
++ assertEquals(3, result.getNumYTiles());
++ assertEquals(4, result.getSampleModel().getNumBands());
++
++ raster = result.getData();
++ assertEquals(4, raster.getNumBands());
++ assertEquals(new Rectangle(minX, minY, width, height), raster.getBounds());
++ assertArrayEquals(new int[] {
++ // Tile 1 ═════════════════╤═════════════════════════╤═══════════════════════ Tile 2 ═════════════════╤═════════════════════════╤════════════════════════
++ 2100, 3100, 4100, 1100, 2101, 3101, 4101, 1101, 2102, 3102, 4102, 1102, 2200, 3200, 4200, 1200, 2201, 3201, 4201, 1201, 2202, 3202, 4202, 1202,
++ 2110, 3110, 4110, 1110, 2111, 3111, 4111, 1111, 2112, 3112, 4112, 1112, 2210, 3210, 4210, 1210, 2211, 3211, 4211, 1211, 2212, 3212, 4212, 1212,
++ 2120, 3120, 4120, 1120, 2121, 3121, 4121, 1121, 2122, 3122, 4122, 1122, 2220, 3220, 4220, 1220, 2221, 3221, 4221, 1221, 2222, 3222, 4222, 1222,
++ // Tile 3 ═════════════════╪═════════════════════════╪═══════════════════════ Tile 4 ═════════════════╪═════════════════════════╪════════════════════════
++ 2300, 3300, 4300, 1300, 2301, 3301, 4301, 1301, 2302, 3302, 4302, 1302, 2400, 3400, 4400, 1400, 2401, 3401, 4401, 1401, 2402, 3402, 4402, 1402,
++ 2310, 3310, 4310, 1310, 2311, 3311, 4311, 1311, 2312, 3312, 4312, 1312, 2410, 3410, 4410, 1410, 2411, 3411, 4411, 1411, 2412, 3412, 4412, 1412,
++ 2320, 3320, 4320, 1320, 2321, 3321, 4321, 1321, 2322, 3322, 4322, 1322, 2420, 3420, 4420, 1420, 2421, 3421, 4421, 1421, 2422, 3422, 4422, 1422,
++ // Tile 5 ═════════════════╪═════════════════════════╪═══════════════════════ Tile 6 ═════════════════╪═════════════════════════╪════════════════════════
++ 2500, 3500, 4500, 1500, 2501, 3501, 4501, 1501, 2502, 3502, 4502, 1502, 2600, 3600, 4600, 1600, 2601, 3601, 4601, 1601, 2602, 3602, 4602, 1602,
++ 2510, 3510, 4510, 1510, 2511, 3511, 4511, 1511, 2512, 3512, 4512, 1512, 2610, 3610, 4610, 1610, 2611, 3611, 4611, 1611, 2612, 3612, 4612, 1612,
++ 2520, 3520, 4520, 1520, 2521, 3521, 4521, 1521, 2522, 3522, 4522, 1522, 2620, 3620, 4620, 1620, 2621, 3621, 4621, 1621, 2622, 3622, 4622, 1622
++ },
++ raster.getPixels(minX, minY, width, height, (int[]) null)
+ );
+ }
+
++ /**
++ * Tests the aggregation of three tiled images having different tile matrices.
++ */
+ @Test
- public void aggregateImagesUsingDifferentExtentsAndDifferentSquaredTiling() {
- // Tip: band number match image tile width. i.e:
- // untiled image -> band 1
- // tiled 2x2 -> bands 2 and 3
- // tiled 4x4 -> bands 4 and 5
- // tiled 6x6 -> band 6
- final TiledImageMock untiled = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 0, 0, 16, 13, 16, 13, 0, 0, true);
- final TiledImageMock tiled2x2 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 2, 4, 2, 8, 10, 2, 2, 0, 0, true);
- final TiledImageMock tiled4x4 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 2, 4, 2, 8, 8, 4, 4, 0, 0, true);
- final TiledImageMock tiled6x6 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 2, 0, 12, 6, 6, 6, 0, 0, true);
-
- init(untiled, tiled2x2, tiled4x4, tiled6x6);
-
- final RenderedImage result = processor().aggregateBands(untiled, tiled2x2, tiled4x4, tiled6x6);
++ @DependsOnMethod("aggregateSimilarlyTiledImages")
++ public void aggregateImagesUsingSameExtentButDifferentTileSizes() {
++ final int minX = 3;
++ final int minY = 1;
++ final int width = 8;
++ final int height = 4;
++ /*
++ * Note: we use different tile indices to test robustness.
++ * The aggregation algorithm should rely on pixel coordinates
++ * for absolute positioning and alignment of image domains.
++ */
++ final TiledImageMock tiled2x2 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 2, 2, 1, 2, true);
++ final TiledImageMock tiled4x1 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 4, 1, 3, 4, true);
++ final TiledImageMock oneTile = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 8, 4, 5, 6, true);
++ initializeAllTiles(tiled2x2, tiled4x1, oneTile);
++
++ final RenderedImage result = processor.aggregateBands(tiled2x2, tiled4x1, oneTile);
+ assertNotNull(result);
- assertArrayEquals(new int[] { 4, 2, 8, 4, 2, 2, 0, 0 }, new int[] {
- result.getMinX(), result.getMinY(), result.getWidth(), result.getHeight(), result.getTileWidth(), result.getTileHeight(), result.getMinTileX(), result.getMinTileY()
- });
++ assertEquals(minX, result.getMinX());
++ assertEquals(minY, result.getMinY());
++ assertEquals(width, result.getWidth());
++ assertEquals(height, result.getHeight());
++ assertEquals(2, result.getTileWidth());
++ assertEquals(1, result.getTileHeight());
++ assertEquals(1, result.getMinTileX());
++ assertEquals(4, result.getMinTileY());
++ assertEquals(4, result.getNumXTiles());
++ assertEquals(4, result.getNumYTiles());
++ assertEquals(3, result.getSampleModel().getNumBands());
+
+ final Raster raster = result.getData();
- assertEquals(new Rectangle(4, 2, 8, 4), raster.getBounds());
++ assertEquals(new Rectangle(minX, minY, width, height), raster.getBounds());
+ assertArrayEquals(
- new int[] {
- 1124, 2100, 3100, 4100, 5100, 6122, 1125, 2101, 3101, 4101, 5101, 6123, 1126, 2200, 3200, 4102, 5102, 6124, 1127, 2201, 3201, 4103, 5103, 6125, 1128, 2300, 3300, 4200, 5200, 6220, 1129, 2301, 3301, 4201, 5201, 6221, 1130, 2400, 3400, 4202, 5202, 6222, 1131, 2401, 3401, 4203, 5203, 6223,
- 1134, 2110, 3110, 4110, 5110, 6132, 1135, 2111, 3111, 4111, 5111, 6133, 1136, 2210, 3210, 4112, 5112, 6134, 1137, 2211, 3211, 4113, 5113, 6135, 1138, 2310, 3310, 4210, 5210, 6230, 1139, 2311, 3311, 4211, 5211, 6231, 1140, 2410, 3410, 4212, 5212, 6232, 1141, 2411, 3411, 4213, 5213, 6233,
-
- 1144, 2500, 3500, 4120, 5120, 6142, 1145, 2501, 3501, 4121, 5121, 6143, 1146, 2600, 3600, 4122, 5122, 6144, 1147, 2601, 3601, 4123, 5123, 6145, 1148, 2700, 3700, 4220, 5220, 6240, 1149, 2701, 3701, 4221, 5221, 6241, 1150, 2800, 3800, 4222, 5222, 6242, 1151, 2801, 3801, 4223, 5223, 6243,
- 1154, 2510, 3510, 4130, 5130, 6152, 1155, 2511, 3511, 4131, 5131, 6153, 1156, 2610, 3610, 4132, 5132, 6154, 1157, 2611, 3611, 4133, 5133, 6155, 1158, 2710, 3710, 4230, 5230, 6250, 1159, 2711, 3711, 4231, 5231, 6251, 1160, 2810, 3810, 4232, 5232, 6252, 1161, 2811, 3811, 4233, 5233, 6253,
- },
- raster.getPixels(4, 2, 8, 4, (int[]) null)
++ new int[] {
++ 1100, 2100, 3100, 1101, 2101, 3101, 1200, 2102, 3102, 1201, 2103, 3103, 1300, 2200, 3104, 1301, 2201, 3105, 1400, 2202, 3106, 1401, 2203, 3107,
++ 1110, 2300, 3110, 1111, 2301, 3111, 1210, 2302, 3112, 1211, 2303, 3113, 1310, 2400, 3114, 1311, 2401, 3115, 1410, 2402, 3116, 1411, 2403, 3117,
++ 1500, 2500, 3120, 1501, 2501, 3121, 1600, 2502, 3122, 1601, 2503, 3123, 1700, 2600, 3124, 1701, 2601, 3125, 1800, 2602, 3126, 1801, 2603, 3127,
++ 1510, 2700, 3130, 1511, 2701, 3131, 1610, 2702, 3132, 1611, 2703, 3133, 1710, 2800, 3134, 1711, 2801, 3135, 1810, 2802, 3136, 1811, 2803, 3137,
++ },
++ raster.getPixels(minX, minY, width, height, (int[]) null)
+ );
+ }
+
- /*
-
- @Test
- public void validateColorModel() {
- throw new UnsupportedOperationException("TODO");
- }
-
- */
- private void aggregateSimilarTiledImages(Boolean firstBanded, Boolean secondBanded) {
- final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_INT, 2, 7, 7, 6, 9, 3, 3, 1, 2, firstBanded);
- final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_INT, 2, 7, 7, 6, 9, 3, 3, 3, 4, secondBanded);
-
- init(im1, im2);
++ /**
++ * Tests the aggregation of three tiled images having different extents and different tile matrices.
++ */
++ @Test
++ @DependsOnMethod("aggregateImagesUsingSameExtentButDifferentTileSizes")
++ public void aggregateImagesUsingDifferentExtentsAndDifferentSquaredTiling() {
++ /*
++ * Tip: band number match image tile width. i.e:
++ *
++ * untiled → band 1
++ * tiled 2x2 → bands 2 and 3
++ * tiled 4x4 → bands 4 and 5
++ * tiled 6x6 → band 6
++ */
++ final TiledImageMock untiled = new TiledImageMock(DataBuffer.TYPE_SHORT, 1, 0, 0, 16, 13, 16, 13, 0, 0, true);
++ final TiledImageMock tiled2x2 = new TiledImageMock(DataBuffer.TYPE_SHORT, 2, 4, 2, 8, 10, 2, 2, 0, 0, true);
++ final TiledImageMock tiled4x4 = new TiledImageMock(DataBuffer.TYPE_SHORT, 2, 4, 2, 8, 8, 4, 4, 0, 0, true);
++ final TiledImageMock tiled6x6 = new TiledImageMock(DataBuffer.TYPE_SHORT, 1, 2, 0, 12, 6, 6, 6, 0, 0, true);
++ initializeAllTiles(untiled, tiled2x2, tiled4x4, tiled6x6);
+
- final ImageProcessor processor = processor();
- RenderedImage result = processor.aggregateBands(im1, im2);
++ final RenderedImage result = processor.aggregateBands(untiled, tiled2x2, tiled4x4, tiled6x6);
+ assertNotNull(result);
- assertArrayEquals(new int[] { 7, 7, 6, 9, 3, 3, 1, 2 }, new int[] { result.getMinX(), result.getMinY(), result.getWidth(), result.getHeight(), result.getTileWidth(), result.getTileHeight(), result.getMinTileX(), result.getMinTileY()});
- Raster raster = result.getData();
- assertEquals(4, raster.getNumBands());
- assertEquals(new Rectangle(7, 7, 6, 9), raster.getBounds());
++ assertEquals(4, result.getMinX());
++ assertEquals(2, result.getMinY());
++ assertEquals(8, result.getWidth());
++ assertEquals(4, result.getHeight());
++ assertEquals(2, result.getTileWidth());
++ assertEquals(2, result.getTileHeight());
++ assertEquals(0, result.getMinTileX());
++ assertEquals(1, result.getMinTileY());
++ assertEquals(4, result.getNumXTiles());
++ assertEquals(2, result.getNumYTiles());
++ assertEquals(6, result.getSampleModel().getNumBands());
+
++ final Raster raster = result.getData();
++ assertEquals(new Rectangle(4, 2, 8, 4), raster.getBounds());
+ assertArrayEquals(
- new int[] {
- // Tile 1 Tile 2
- 1100, 2100, 3100, 4100, 1101, 2101, 3101, 4101, 1102, 2102, 3102, 4102, 1200, 2200, 3200, 4200, 1201, 2201, 3201, 4201, 1202, 2202, 3202, 4202,
- 1110, 2110, 3110, 4110, 1111, 2111, 3111, 4111, 1112, 2112, 3112, 4112, 1210, 2210, 3210, 4210, 1211, 2211, 3211, 4211, 1212, 2212, 3212, 4212,
- 1120, 2120, 3120, 4120, 1121, 2121, 3121, 4121, 1122, 2122, 3122, 4122, 1220, 2220, 3220, 4220, 1221, 2221, 3221, 4221, 1222, 2222, 3222, 4222,
- // Tile 3 Tile 4
- 1300, 2300, 3300, 4300, 1301, 2301, 3301, 4301, 1302, 2302, 3302, 4302, 1400, 2400, 3400, 4400, 1401, 2401, 3401, 4401, 1402, 2402, 3402, 4402,
- 1310, 2310, 3310, 4310, 1311, 2311, 3311, 4311, 1312, 2312, 3312, 4312, 1410, 2410, 3410, 4410, 1411, 2411, 3411, 4411, 1412, 2412, 3412, 4412,
- 1320, 2320, 3320, 4320, 1321, 2321, 3321, 4321, 1322, 2322, 3322, 4322, 1420, 2420, 3420, 4420, 1421, 2421, 3421, 4421, 1422, 2422, 3422, 4422,
- // Tile 5 Tile 6
- 1500, 2500, 3500, 4500, 1501, 2501, 3501, 4501, 1502, 2502, 3502, 4502, 1600, 2600, 3600, 4600, 1601, 2601, 3601, 4601, 1602, 2602, 3602, 4602,
- 1510, 2510, 3510, 4510, 1511, 2511, 3511, 4511, 1512, 2512, 3512, 4512, 1610, 2610, 3610, 4610, 1611, 2611, 3611, 4611, 1612, 2612, 3612, 4612,
- 1520, 2520, 3520, 4520, 1521, 2521, 3521, 4521, 1522, 2522, 3522, 4522, 1620, 2620, 3620, 4620, 1621, 2621, 3621, 4621, 1622, 2622, 3622, 4622
- },
- raster.getPixels(7, 7, 6, 9, (int[]) null)
- );
-
- // Repeat the test with a custom band selection.
- result = processor.aggregateBands(Arrays.asList(im1, im2, im1), Arrays.asList(null, new int[] { 1 }, new int[] { 0 }), null);
- assertNotNull(result);
- assertArrayEquals(new int[] { 7, 7, 6, 9, 3, 3, 1, 2 }, new int[] { result.getMinX(), result.getMinY(), result.getWidth(), result.getHeight(), result.getTileWidth(), result.getTileHeight(), result.getMinTileX(), result.getMinTileY()});
- raster = result.getData();
- assertEquals(4, raster.getNumBands());
- assertEquals(new Rectangle(7, 7, 6, 9), raster.getBounds());
++ new int[] {
++ 1124, 2100, 3100, 4100, 5100, 6122, 1125, 2101, 3101, 4101, 5101, 6123, 1126, 2200, 3200, 4102, 5102, 6124, 1127, 2201, 3201, 4103, 5103, 6125, 1128, 2300, 3300, 4200, 5200, 6220, 1129, 2301, 3301, 4201, 5201, 6221, 1130, 2400, 3400, 4202, 5202, 6222, 1131, 2401, 3401, 4203, 5203, 6223,
++ 1134, 2110, 3110, 4110, 5110, 6132, 1135, 2111, 3111, 4111, 5111, 6133, 1136, 2210, 3210, 4112, 5112, 6134, 1137, 2211, 3211, 4113, 5113, 6135, 1138, 2310, 3310, 4210, 5210, 6230, 1139, 2311, 3311, 4211, 5211, 6231, 1140, 2410, 3410, 4212, 5212, 6232, 1141, 2411, 3411, 4213, 5213, 6233,
+
- assertArrayEquals(
- new int[] {
- // Tile 1 Tile 2
- 1100, 2100, 4100, 1100, 1101, 2101, 4101, 1101, 1102, 2102, 4102, 1102, 1200, 2200, 4200, 1200, 1201, 2201, 4201, 1201, 1202, 2202, 4202, 1202,
- 1110, 2110, 4110, 1110, 1111, 2111, 4111, 1111, 1112, 2112, 4112, 1112, 1210, 2210, 4210, 1210, 1211, 2211, 4211, 1211, 1212, 2212, 4212, 1212,
- 1120, 2120, 4120, 1120, 1121, 2121, 4121, 1121, 1122, 2122, 4122, 1122, 1220, 2220, 4220, 1220, 1221, 2221, 4221, 1221, 1222, 2222, 4222, 1222,
- // Tile 3 Tile 4
- 1300, 2300, 4300, 1300, 1301, 2301, 4301, 1301, 1302, 2302, 4302, 1302, 1400, 2400, 4400, 1400, 1401, 2401, 4401, 1401, 1402, 2402, 4402, 1402,
- 1310, 2310, 4310, 1310, 1311, 2311, 4311, 1311, 1312, 2312, 4312, 1312, 1410, 2410, 4410, 1410, 1411, 2411, 4411, 1411, 1412, 2412, 4412, 1412,
- 1320, 2320, 4320, 1320, 1321, 2321, 4321, 1321, 1322, 2322, 4322, 1322, 1420, 2420, 4420, 1420, 1421, 2421, 4421, 1421, 1422, 2422, 4422, 1422,
- // Tile 5 Tile 6
- 1500, 2500, 4500, 1500, 1501, 2501, 4501, 1501, 1502, 2502, 4502, 1502, 1600, 2600, 4600, 1600, 1601, 2601, 4601, 1601, 1602, 2602, 4602, 1602,
- 1510, 2510, 4510, 1510, 1511, 2511, 4511, 1511, 1512, 2512, 4512, 1512, 1610, 2610, 4610, 1610, 1611, 2611, 4611, 1611, 1612, 2612, 4612, 1612,
- 1520, 2520, 4520, 1520, 1521, 2521, 4521, 1521, 1522, 2522, 4522, 1522, 1620, 2620, 4620, 1620, 1621, 2621, 4621, 1621, 1622, 2622, 4622, 1622
- },
- raster.getPixels(7, 7, 6, 9, (int[]) null)
++ 1144, 2500, 3500, 4120, 5120, 6142, 1145, 2501, 3501, 4121, 5121, 6143, 1146, 2600, 3600, 4122, 5122, 6144, 1147, 2601, 3601, 4123, 5123, 6145, 1148, 2700, 3700, 4220, 5220, 6240, 1149, 2701, 3701, 4221, 5221, 6241, 1150, 2800, 3800, 4222, 5222, 6242, 1151, 2801, 3801, 4223, 5223, 6243,
++ 1154, 2510, 3510, 4130, 5130, 6152, 1155, 2511, 3511, 4131, 5131, 6153, 1156, 2610, 3610, 4132, 5132, 6154, 1157, 2611, 3611, 4133, 5133, 6155, 1158, 2710, 3710, 4230, 5230, 6250, 1159, 2711, 3711, 4231, 5231, 6251, 1160, 2810, 3810, 4232, 5232, 6252, 1161, 2811, 3811, 4233, 5233, 6253,
++ },
++ raster.getPixels(4, 2, 8, 4, (int[]) null)
+ );
+ }
+
- private ImageProcessor processor() { return new ImageProcessor(); }
-
+ /**
- * Initialize all bands of all input images with a "BTYX" pattern where:
++ * Initializes all bands of all input images to testing values.
++ * The testing values are defined by a "BTYX" pattern where:
+ * <ol>
- * <li>
- * "B" is the band index over all encountered images.
- * It means that be start at 1 for the first band of first encountered image,
- * and then it is incremented for each band of each encountered image
- * </li>
- * <li>"TYX" is defined by {@link TiledImageMock#initializeAllTiles(int...)}</li>
++ * <li><var>B</var> is the band index over all encountered image. It starts at 1 for the first
++ * band of first encountered image, then incremented for each band of each encountered image.</li>
++ * <li><var>T</var> is the tile index starting with 1 for the first tile and increasing in a row-major fashion.</li>
++ * <li><var>Y</var> is the <var>y</var> coordinate (row 0-based index) of the sample value relative to current tile.</li>
++ * <li><var>X</var> is the <var>x</var> coordinate (column 0-based index) of the sample value relative to current tile.</li>
+ * </ol>
+ */
- private void init(TiledImageMock... images) {
- int b = 1;
- for (TiledImageMock image : images) {
- int[] allBands = IntStream.range(0, image.getSampleModel().getNumBands()).toArray();
- image.initializeAllTiles(allBands);
- final int cursor = b;
- updateValues(image, (band, sample) -> (cursor + band) * 1000 + sample);
- b += allBands.length;
- }
- }
-
- /**
- * Change pixel values of input image with provided binary operator.
- *
- * @param target The image to update (mutated directly)
- * @param valueUpdate Operator updating current sample value.
- * It receives as input the current band index (0-based) and the value associated to this band on the current pixel (the current sample value).
- * It must return the new value to associate to the pixel.
- */
- private void updateValues(WritableRenderedImage target, IntBinaryOperator valueUpdate) {
- try (WritablePixelIterator it = new PixelIterator.Builder().createWritable(target)) {
- int[] pixel = new int[it.getNumBands()];
- while (it.next()) {
- it.getPixel(pixel);
- for (int i = 0; i < pixel.length; i++) pixel[i] = valueUpdate.applyAsInt(i, pixel[i]);
- it.setPixel(pixel);
- }
++ private static void initializeAllTiles(final TiledImageMock... images) {
++ int band = 0;
++ for (final TiledImageMock image : images) {
++ final int numBands = image.getSampleModel().getNumBands();
++ image.initializeAllTiles(ArraysExt.range(0, numBands), band * 1000);
++ band += numBands;
+ }
+ }
+ }
-
diff --cc core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
index bad1958f09,cff143d4b6..8251583681
--- a/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
@@@ -16,6 -16,6 +16,10 @@@
*/
package org.apache.sis.image;
++import java.util.Arrays;
++import java.util.Random;
++import java.util.stream.Collectors;
++import java.util.concurrent.atomic.AtomicInteger;
import java.awt.Point;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
@@@ -32,8 -32,10 +36,6 @@@ import java.awt.image.SampleModel
import java.awt.image.TileObserver;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
-import java.util.Arrays;
--import java.util.Random;
--import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.coverage.j2d.WritableTiledImage;
import org.apache.sis.internal.util.Numerics;
@@@ -49,7 -51,7 +51,8 @@@ import static org.junit.Assert.*
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.1
++ * @author Alexis Manin (Geomatys)
++ * @version 1.4
* @since 0.8
*/
public final class TiledImageMock extends PlanarImage implements WritableRenderedImage {
@@@ -270,29 -272,43 +273,49 @@@
/**
* Initializes the sample values of all tiles to testing values.
- * The sample values will be 3 digits numbers of the form "TXY" where:
- * The sample values will be 3 digits numbers of the form "TYX" where:
++ * The sample values will be 4 digits numbers of the form "BTYX" where:
* <ul>
++ * <li><var>B</var> is the band index starting with 1 for the first band.</li>
* <li><var>T</var> is the tile index starting with 1 for the first tile and increasing in a row-major fashion.</li>
- * <li><var>X</var> is the <var>x</var> coordinate (column index) of the sample value relative to current tile.</li>
- * <li><var>Y</var> is the <var>y</var> coordinate (row index) of the sample value relative to current tile.</li>
+ * <li><var>Y</var> is the <var>y</var> coordinate (row 0-based index) of the sample value relative to current tile.</li>
+ * <li><var>X</var> is the <var>x</var> coordinate (column 0-based index) of the sample value relative to current tile.</li>
* </ul>
*
- * The "TXY" pattern holds if all values are less than 10. If some values are greater than 10,
- * then the sample values are a mix of above values resulting from arithmetic sums.
- * The "TYX" pattern holds if all values are less than 10. If some values are greater than 10,
++ * The "BTYX" pattern holds if all values are less than 10. If some values are greater than 10,
+ * then the sample values are a mix of above values resulting from the following arithmetic sum:
- * {@code T * 100 + Y * 10 + X}.
- * <br>
- * All bands will be affected the same value.
- * <br>
- * <em>Important</em>: This method resets all tiles of this image. It means that:
- * <ul>
- * <li>All previous modifications will be erased</li>
- * <li>Any band omitted as input will be erased. This is true even when omitting all tiles.</li>
- * </ul>
- * @param bands band index where to set values. If none provided, all bands are <em>emptied</em>,
- * i.e. a fresh raster is created for each tile, but not filled.
++ *
++ * <blockquote>
++ * <var>sample</var> = <var>B</var> * 1000 + <var>T</var> * 100 + <var>Y</var> * 10 + <var>X</var>
++ * </blockquote>
+ *
- * @param band band index where to set values. Other bands will be unmodified.
++ * This method creates a new raster for each tile.
++ * Any modification to previously existing rasters will be lost.
++ * Bands that are not specified in the {@code bands} array will have their values initialized to zero.
++ *
++ * @param bands indices of the bands where to apply the "BTYX" pattern. Other bands will be initialized to zero.
++ * @param offset an arbitrary offset to add to all values.
++ *
++ * @since 1.4
*/
- public synchronized void initializeAllTiles(final int band) {
- public synchronized void initializeAllTiles(int... bands) {
- if (bands == null) bands = new int[] {};
- else if (Arrays.stream(bands).boxed().collect(Collectors.toSet()).size() < bands.length) {
- throw new IllegalArgumentException("Input band list contain doublons. Please verify your input.");
++ public synchronized void initializeAllTiles(final int[] bands, final int offset) {
++ if (bands.length > 1) { // Skip the following check for the common case of a single band.
++ if (Arrays.stream(bands).boxed().collect(Collectors.toSet()).size() != bands.length) {
++ throw new IllegalArgumentException("Input band list contains duplicated values.");
++ }
+ }
int ti = 0;
for (int ty=0; ty<numYTiles; ty++) {
for (int tx=0; tx<numXTiles; tx++) {
-- final int value = (ti + 1) * 100;
++ final int first = (ti + 1) * 100 + offset;
final int x = tx * tileWidth + minX;
final int y = ty * tileHeight + minY;
final WritableRaster raster = Raster.createWritableRaster(sampleModel, new Point(x, y));
for (int j=0; j<tileHeight; j++) {
for (int i=0; i<tileWidth; i++) {
- raster.setSample(x+i, y+j, band, value + 10*j + i);
- final int pixelValue = value + 10 * j + i;
- for (int band : bands) raster.setSample(x+i, y+j, band, pixelValue);
++ final int sample = first + 10 * j + i;
++ for (final int band : bands) {
++ raster.setSample(x+i, y+j, band, (band + 1) * 1000 + sample);
++ }
}
}
tiles[ti++] = raster;
@@@ -301,9 -317,9 +324,28 @@@
assertEquals(tiles.length, ti);
}
++ /**
++ * Initializes the sample values of all tiles to testing values in a single band.
++ * The sample values will be 3 digits numbers of the form "TYX" where:
++ * <ul>
++ * <li><var>T</var> is the tile index starting with 1 for the first tile and increasing in a row-major fashion.</li>
++ * <li><var>Y</var> is the <var>y</var> coordinate (row 0-based index) of the sample value relative to current tile.</li>
++ * <li><var>X</var> is the <var>x</var> coordinate (column 0-based index) of the sample value relative to current tile.</li>
++ * </ul>
++ *
++ * The "TYX" pattern holds if all values are less than 10. If some values are greater than 10,
++ * then the sample values are a mix of above values resulting from the arithmetic sum defined
++ * in {@link #initializeAllTiles(int[], int)}.
++ *
++ * @param band index of the band where to apply the "TYX" pattern. Other bands will be initialized to zero.
++ */
++ public void initializeAllTiles(final int band) {
++ initializeAllTiles(new int[] {band}, (band + 1) * -1000); // Offset cancels the "B" in "BTYX" pattern.
++ }
++
/**
* Initializes the sample values of all tiles to random values. The image must have been
- * initialized by a call to {@link #initializeAllTiles(int...)} before to invoke this method.
+ * initialized by a call to {@link #initializeAllTiles(int)} before to invoke this method.
*
* @param band band index where to set values. Other bands will be unmodified.
* @param generator the random number generator to use for obtaining values.
diff --cc core/sis-feature/src/test/java/org/apache/sis/internal/coverage/RangeArgumentTest.java
index c10d74351d,c10d74351d..47f4b50b28
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/RangeArgumentTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/RangeArgumentTest.java
@@@ -14,15 -14,15 +14,10 @@@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
--package org.apache.sis.internal.storage;
++package org.apache.sis.internal.coverage;
--import java.util.List;
import java.util.Locale;
--import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.grid.GridExtent;
--import org.apache.sis.coverage.grid.GridGeometry;
--import org.apache.sis.coverage.grid.GridCoverage;
--import org.apache.sis.storage.AbstractGridCoverageResource;
import org.apache.sis.test.TestCase;
import org.apache.sis.util.Localized;
import org.junit.Test;
@@@ -34,19 -34,19 +29,10 @@@ import static org.junit.Assert.*
* Tests {@link RangeArgument}.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.0
*/
public final class RangeArgumentTest extends TestCase implements Localized {
-- /**
-- * A resource performing no operation.
-- */
-- private final AbstractGridCoverageResource resource = new AbstractGridCoverageResource(null, false) {
-- @Override public GridGeometry getGridGeometry() {throw new UnsupportedOperationException();}
-- @Override public List<SampleDimension> getSampleDimensions() {throw new UnsupportedOperationException();}
-- @Override public GridCoverage read(GridGeometry d, int... r) {throw new UnsupportedOperationException();}
-- };
--
/**
* Returns a fixed locale for testing purpose.
*
diff --cc core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index aa285fcdcf,758ecbb4d5..571725f8ee
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@@ -83,6 -82,6 +83,7 @@@ import org.junit.runners.Suite
org.apache.sis.feature.builder.FeatureTypeBuilderTest.class,
// Rasters
++ org.apache.sis.internal.coverage.RangeArgumentTest.class,
org.apache.sis.internal.coverage.j2d.ImageUtilitiesTest.class,
org.apache.sis.internal.coverage.j2d.ImageLayoutTest.class,
org.apache.sis.internal.coverage.j2d.ScaledColorSpaceTest.class,
@@@ -121,6 -121,6 +123,7 @@@
org.apache.sis.coverage.grid.ConvertedGridCoverageTest.class,
org.apache.sis.coverage.grid.TranslatedGridCoverageTest.class,
org.apache.sis.coverage.grid.ResampledGridCoverageTest.class,
++ org.apache.sis.coverage.grid.DimensionalityReductionTest.class,
// Index and processing
org.apache.sis.index.tree.PointTreeNodeTest.class,
diff --cc core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index f1171ae393,f1171ae393..fa26a03e72
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@@ -958,7 -958,7 +958,7 @@@ public final class CRS extends Static
* @param dimensions the dimensions to retain. The dimensions will be taken in increasing order, ignoring duplicated values.
* @return a coordinate reference system for the given dimensions. May be the given {@code crs}, which may be {@code null}.
* @throws IllegalArgumentException if the given array is empty or if the array contains invalid indices.
-- * @throws FactoryException if the geodetic factory failed to create a compound CRS.
++ * @throws FactoryException if this method needed to create a new CRS and that operation failed.
*
* @see #getComponentAt(CoordinateReferenceSystem, int, int)
* @see #compound(CoordinateReferenceSystem...)
@@@ -967,10 -967,10 +967,40 @@@
*/
public static CoordinateReferenceSystem selectDimensions(final CoordinateReferenceSystem crs,
final int... dimensions) throws FactoryException
++ {
++ final var components = selectComponents(crs, dimensions);
++ if (components.isEmpty()) {
++ return null;
++ }
++ return compound(components.toArray(CoordinateReferenceSystem[]::new));
++ }
++
++ /**
++ * Gets or creates CRS components for a subset of the dimensions of the given CRS.
++ * The method performs the same work as {@link #selectDimensions(CoordinateReferenceSystem, int...)}
++ * except that it does not build new {@link CompoundCRS} instances when the specified dimensions span
++ * more than one {@linkplain DefaultCompoundCRS#getComponents() component}.
++ * Instead, the components are returned directly.
++ *
++ * <p>While this method does not create new {@code CompoundCRS} instances, it still may create other
++ * kinds of CRS for handling ellipsoidal height as documented in the {@code selectDimensions(…)} method.</p>
++ *
++ * @param crs the CRS from which to get a subset of the components, or {@code null} if none.
++ * @param dimensions the dimensions to retain. The dimensions will be taken in increasing order, ignoring duplicated values.
++ * @return components in the specified dimensions, or an empty list if the specified {@code crs} is {@code null}.
++ * @throws IllegalArgumentException if the given array is empty or if the array contains invalid indices.
++ * @throws FactoryException if this method needed to create a new CRS and that operation failed.
++ *
++ * @see #selectDimensions(CoordinateReferenceSystem, int...)
++ *
++ * @since 1.4
++ */
++ public static List<CoordinateReferenceSystem> selectComponents(final CoordinateReferenceSystem crs,
++ final int... dimensions) throws FactoryException
{
ArgumentChecks.ensureNonNull("dimensions", dimensions);
if (crs == null) {
-- return null;
++ return List.of();
}
final int dimension = ReferencingUtilities.getDimension(crs);
long selected = 0;
@@@ -988,7 -988,7 +1018,7 @@@
}
final List<CoordinateReferenceSystem> components = new ArrayList<>(Long.bitCount(selected));
reduce(0, crs, dimension, selected, components);
-- return compound(components.toArray(CoordinateReferenceSystem[]::new));
++ return components;
}
/**
@@@ -1367,7 -1367,7 +1397,7 @@@
* or cannot be decomposed for dimensions in the [{@code lower} … {@code upper}] range.
* @throws IndexOutOfBoundsException if the given index are out of bounds.
*
-- * @see #selectDimensions(CoordinateReferenceSystem, int[])
++ * @see #selectDimensions(CoordinateReferenceSystem, int...)
* @see org.apache.sis.geometry.GeneralEnvelope#subEnvelope(int, int)
*
* @since 0.5
diff --cc core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
index c0b2b3c047,c0b2b3c047..27a1f360d5
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@@ -18,6 -18,6 +18,7 @@@ package org.apache.sis.referencing.oper
import java.util.Map;
import java.util.List;
++import java.util.BitSet;
import java.util.Optional;
import java.awt.geom.AffineTransform;
import org.opengis.util.FactoryException;
@@@ -361,6 -361,6 +362,40 @@@ public final class MathTransforms exten
return PassThroughTransform.create(firstAffectedCoordinate, subTransform, numTrailingCoordinates);
}
++ /**
++ * Creates a transform which passes through a subset of coordinates to another transform.
++ * The list of modified coordinates is specified by the {@code modifiedCoordinates} argument,
++ * which must be in strictly increasing order.
++ *
++ * @param modifiedCoordinates positions in a source coordinate tuple of the coordinates affected by the transform.
++ * @param subTransform the sub-transform to apply on modified coordinates.
++ * @param resultDim total number of source dimensions of the pass-through transform to return.
++ * @return a pass-through transform for the given set of modified coordinates.
++ * @throws MismatchedDimensionException if the bit set cardinality is not equal
++ * to the number of source dimensions in {@code subTransform}.
++ * @throws IllegalArgumentException if the index of a modified coordinates is invalid.
++ *
++ * @see PassThroughTransform#create(BitSet, MathTransform, int, MathTransformFactory)
++ *
++ * @since 1.4
++ */
++ public static MathTransform passThrough(final int[] modifiedCoordinates, final MathTransform subTransform, final int resultDim) {
++ final BitSet bitset = new BitSet();
++ int previous = -1;
++ for (int i=0; i < modifiedCoordinates.length; i++) {
++ final int dim = modifiedCoordinates[i];
++ String message = TransformSeparator.validate("modifiedCoordinates", i, previous, resultDim, dim);
++ if (message != null) throw new IllegalArgumentException(message);
++ bitset.set(dim);
++ previous = dim;
++ }
++ try {
++ return PassThroughTransform.create(bitset, subTransform, resultDim, null);
++ } catch (FactoryException e) {
++ throw new IllegalArgumentException(e); // Should never happen actually.
++ }
++ }
++
/**
* Puts together a list of independent math transforms, each of them operating on a subset of coordinate values.
* This method is often used for defining 4-dimensional (<var>x</var>,<var>y</var>,<var>z</var>,<var>t</var>)
diff --cc core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
index 38107bf365,38107bf365..d6d8bc5d10
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
@@@ -17,10 -17,10 +17,12 @@@
package org.apache.sis.referencing.operation.transform;
import java.util.Arrays;
++import java.util.BitSet;
import java.io.Serializable;
import java.lang.reflect.Array;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
++import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
@@@ -34,11 -34,11 +36,11 @@@ import org.apache.sis.internal.referenc
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.io.wkt.Formatter;
++import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.ArraysExt;
--
--import static org.apache.sis.util.ArgumentChecks.*;
++import org.apache.sis.util.resources.Errors;
/**
@@@ -65,7 -65,7 +67,7 @@@
* Serialization should be used only for short term storage or RMI between applications running the same SIS version.
*
* @author Martin Desruisseaux (IRD, Geomatys)
-- * @version 1.0
++ * @version 1.4
*
* @see MathTransforms#passThrough(int, MathTransform, int)
* @see MathTransforms#compound(MathTransform...)
@@@ -121,8 -121,8 +123,8 @@@ public class PassThroughTransform exten
final MathTransform subTransform,
final int numTrailingCoordinates)
{
-- ensurePositive("firstAffectedCoordinate", firstAffectedCoordinate);
-- ensurePositive("numTrailingCoordinates", numTrailingCoordinates);
++ ArgumentChecks.ensurePositive("firstAffectedCoordinate", firstAffectedCoordinate);
++ ArgumentChecks.ensurePositive("numTrailingCoordinates", numTrailingCoordinates);
if (subTransform instanceof PassThroughTransform) {
final PassThroughTransform passThrough = (PassThroughTransform) subTransform;
this.firstAffectedCoordinate = passThrough.firstAffectedCoordinate + firstAffectedCoordinate;
@@@ -148,7 -148,7 +150,7 @@@
* {@code dimTarget - numTrailingCoordinates} exclusive.
*
* @param firstAffectedCoordinate index of the first affected coordinate.
-- * @param subTransform the sub-transform to apply on modified coordinates.
++ * @param subTransform the sub-transform to apply on modified coordinates.
* @param numTrailingCoordinates number of trailing coordinates to pass through.
* @return a pass-through transform, not necessarily a {@code PassThroughTransform} instance.
*/
@@@ -160,8 -160,8 +162,8 @@@
/*
* Above checks tried to avoid the creation of PassThroughTransform instance. At this point we cannot
* avoid it anymore. But maybe we can merge two PassThroughTransforms into a single one. It may happen
-- * if 'subTransform' is a concatenation of a linear transform + pass through transform (in any order).
-- * In such case, moving the linear transform outside 'subTransform' enable above-cited merge.
++ * if `subTransform` is a concatenation of a linear transform + pass through transform (in any order).
++ * In such case, moving the linear transform outside `subTransform` enable above-cited merge.
*/
if (subTransform instanceof ConcatenatedTransform) {
MathTransform transform1 = ((ConcatenatedTransform) subTransform).transform1;
@@@ -210,6 -210,6 +212,75 @@@
return new PassThroughTransform(firstAffectedCoordinate, subTransform, numTrailingCoordinates);
}
++ /**
++ * Creates a transform which passes through a subset of coordinates to another transform.
++ * The list of modified coordinates is specified by a {@link BitSet} argument where each
++ * bit set to 1 identifies the dimension of a modified coordinate.
++ * The array of modified coordinates can be expanded as below:
++ *
++ * {@snippet lang="java" :
++ * int[] modifiedCoordinates = bitset.stream().toArray();
++ * }
++ *
++ * <h4>Limitation</h4>
++ * If the modified coordinates are not at consecutive positions in source coordinate tuples,
++ * then the current implementation of this method adds the following restrictions:
++ *
++ * <ul>
++ * <li>The sub-transform must have an number of target dimensions equal to the number of source dimensions.</li>
++ * <li>The sub-transform must be {@linkplain TransformSeparator separable}.</li>
++ * </ul>
++ *
++ * Above restrictions are relaxed if all modified coordinates are at consecutive positions.
++ *
++ * @param modifiedCoordinates positions in a source coordinate tuple of the coordinates affected by the transform.
++ * @param subTransform the sub-transform to apply on modified coordinates.
++ * @param resultDim total number of source dimensions of the pass-through transform to return.
++ * @param factory the factory to use for creating transforms, or {@code null} for the default.
++ * @return a pass-through transform for the given set of modified coordinates.
++ * @throws MismatchedDimensionException if the bit set cardinality is not equal
++ * to the number of source dimensions in {@code subTransform}.
++ * @throws IllegalArgumentException if the index of a modified coordinates is out of bounds.
++ * @throws FactoryException if an error occurred while creating a transform step.
++ *
++ * @see MathTransforms#passThrough(int[], MathTransform, int)
++ *
++ * @since 1.4
++ */
++ public static MathTransform create(final BitSet modifiedCoordinates, final MathTransform subTransform, final int resultDim,
++ final MathTransformFactory factory) throws FactoryException
++ {
++ ArgumentChecks.ensurePositive("resultDim", resultDim);
++ final int subDim = subTransform.getSourceDimensions();
++ final int actual = modifiedCoordinates.cardinality();
++ if (actual != subDim) {
++ throw new MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_3,
++ "modifiedCoordinates", subDim, actual));
++ }
++ final var sep = new TransformSeparator(subTransform, factory);
++ MathTransform result = MathTransforms.identity(resultDim);
++ int lower, upper = 0, subLower = 0;
++ while ((lower = modifiedCoordinates.nextSetBit(upper)) >= 0) {
++ upper = modifiedCoordinates.nextClearBit(lower);
++ final int subUpper = subLower + (upper - lower);
++ MathTransform step;
++ if (subLower == 0 && subUpper == subDim) {
++ step = subTransform;
++ } else {
++ // Restriction below apply only if the `subTransform` can not be used as a whole.
++ ArgumentChecks.ensureDimensionsMatch("subTransform", subDim, subDim, subTransform);
++ sep.addSourceDimensionRange(subLower, subUpper);
++ sep.addTargetDimensionRange(subLower, subUpper);
++ step = sep.separate();
++ }
++ step = sep.factory.passThrough(lower, step, resultDim - upper);
++ result = sep.factory.concatenate(result, step);
++ sep.clear();
++ subLower = subUpper;
++ }
++ return result;
++ }
++
/**
* Gets the dimension of input points. This the source dimension of the
* {@linkplain #subTransform sub-transform} plus the number of pass-through dimensions.
@@@ -236,13 -236,13 +307,6 @@@
* Returns the ordered sequence of positive integers defining the positions in a source
* coordinate tuple of the coordinates affected by this pass-through operation.
*
-- * <div class="note"><b>API note:</b> this method is final for now because most of Apache SIS code do
-- * not use the {@code modifiedCoordinates} array. Instead, SIS uses the {@code firstAffectedCoordinate}
-- * and {@code numTrailingCoordinates} information provided to the constructor. Consequently, overriding
-- * this method may be misleading since it would be ignored by SIS. We do not want to make the "really
-- * used" fields public in order to keep the flexibility to replace them by a {@code modifiedCoordinates}
-- * array in a future SIS version.</div>
-- *
* @return Zero-based indices of the modified source coordinates.
*
* @see org.apache.sis.referencing.operation.DefaultPassThroughOperation#getModifiedCoordinates()
@@@ -324,11 -324,11 +388,11 @@@
final int dimSource = subDimSource + numPassThrough;
final int dimTarget = subDimTarget + numPassThrough;
/*
-- * Copy the pass-through coordinates (both before and after the sub-transform) into the 'pasPts'
++ * Copy the pass-through coordinates (both before and after the sub-transform) into the `pasPts`
* temporary array. This will allow us to compact the coordinates to give to the sub-transform,
-- * so we can process them in a single 'transform' method call. We do that also for avoiding tricky
++ * so we can process them in a single `transform` method call. We do that also for avoiding tricky
* issues with overlapping regions, because coordinate tuples are not processed automically the
-- * way 'IterationStrategy' expects.
++ * way `IterationStrategy` expects.
*/
final Object pasPts;
{
@@@ -400,8 -400,8 +464,8 @@@
}
/*
* Copies the transformed coordinates to their final location, inserting pass-through
-- * coordinates between them in the process. Note that we avoided to modify 'dstOff'
-- * and 'numPts' before this point, but now we are free to do so since this is the last
++ * coordinates between them in the process. Note that we avoided to modify `dstOff`
++ * and `numPts` before this point, but now we are free to do so since this is the last
* step.
*/
int pasOff = numPts * numPassThrough;
@@@ -501,7 -501,7 +565,7 @@@
public Matrix derivative(final DirectPosition point) throws TransformException {
final int nSkipped = firstAffectedCoordinate + numTrailingCoordinates;
final int transDim = subTransform.getSourceDimensions();
-- ensureDimensionMatches("point", transDim + nSkipped, point);
++ ArgumentChecks.ensureDimensionMatches("point", transDim + nSkipped, point);
final GeneralDirectPosition subPoint = new GeneralDirectPosition(transDim);
for (int i=0; i<transDim; i++) {
subPoint.coordinates[i] = point.getOrdinate(i + firstAffectedCoordinate);
@@@ -736,9 -736,9 +800,9 @@@
return other; // Shortcut avoiding creation of new MathTransforms.
}
/*
-- * We enter in this block if some dimensions can be discarded. We want to discard them before the
-- * PassThroughTransform instead of after. The matrix for that purpose will be computed later.
-- * Before that, the loop below modifies a copy of the 'other' matrix as if those dimensions were
++ * We enter in this block if some dimensions can be discarded. We want to discard them before
++ * the PassThroughTransform instead of after. The matrix for that purpose will be computed later.
++ * Before that, the loop below modifies a copy of the `other` matrix as if those dimensions were
* already removed.
*/
MatrixSIS reduced = MatrixSIS.castOrCopy(m);
@@@ -751,17 -751,17 +815,17 @@@
columnsToRemove >>>= (upper - lower);
} while (columnsToRemove != 0);
/*
-- * Expands the 'retainedDimensions' bitmask into a list of indices of dimensions to keep. However
++ * Expands the `retainedDimensions` bitmask into a list of indices of dimensions to keep. However
* those indices are for dimensions to keep after the PassThroughTransform. Because we rather want
* indices for dimensions to keep before the PassThroughTransform, we need to adjust for difference
-- * in number of dimensions. This change is represented by the 'change' integer computed above.
++ * in number of dimensions. This change is represented by the `change` integer computed above.
* We apply two strategies:
*
-- * 1) If we keep the sub-transform, then the loop while surely sees the 'firstAffectedCoordinate'
++ * 1) If we keep the sub-transform, then the loop while surely sees the `firstAffectedCoordinate`
* dimension since we ensured that we keep all sub-transform dimensions. When it happens, we
* add or remove bits at that point for the dimensionality changes.
*
-- * 2) If we do not keep the sub-transform, then code inside 'if (dim == firstAffectedCoordinate)'
++ * 2) If we do not keep the sub-transform, then code inside `if (dim == firstAffectedCoordinate)`
* should not have been executed. Instead, we will adjust the indices after the loop.
*/
final long leadPassThroughMask = maskLowBits(firstAffectedCoordinate);
@@@ -793,7 -793,7 +857,7 @@@
* Concatenate:
* 1) An affine transform discarding some dimensions (no other operation).
* 2) The passthrough transform with less input and output dimensions.
-- * 3) The 'other' transform with less input dimensions.
++ * 3) The `other` transform with less input dimensions.
*/
MathTransform tr = proxy.linear(Matrices.createDimensionSelect(dimension + change, indices));
if (keepSubTransform) {
@@@ -808,7 -808,7 +872,7 @@@
/*
* Do not invoke super.tryConcatenate(applyOtherFirst, other, factory); we do not want to test if this transform
* is the inverse of the other transform as it is costly and unnecessary. If it was the case, the concatenation
-- * of 'this.subTransform' with 'other.subTransform' done at the beginning of this method would have produced the
++ * of `this.subTransform` with `other.subTransform` done at the beginning of this method would have produced the
* identity transform already.
*/
return null;
diff --cc core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
index 70fb15dfe5,70fb15dfe5..4ebfacd07d
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
@@@ -94,7 -94,7 +94,7 @@@ public class TransformSeparator
/**
* The factory to use for creating new math transforms.
*/
-- private final MathTransformsOrFactory factory;
++ final MathTransformsOrFactory factory;
/**
* Whether {@link #separate()} is allowed to add new dimensions in {@link #sourceDimensions}
@@@ -191,16 -191,16 +191,31 @@@
* We verify after the copy as a paranoiac safety against concurrent changes.
*/
for (int i=offset; i<sequence.length; i++) {
-- final int value = sequence[i];
-- if (value <= previous || value >= max) {
-- throw new IllegalArgumentException(Errors.format(Errors.Keys.ValueOutOfRange_4,
-- Strings.toIndexed("dimensions", i - offset), previous + 1, max - 1, value));
-- }
-- previous = value;
++ String message = validate("dimensions", i - offset, previous, max, previous = sequence[i]);
++ if (message != null) throw new IllegalArgumentException(message);
}
return sequence;
}
++ /**
++ * If the given value is out of bounds, returns the error message for the exception to throw.
++ * This is used during validation of an array expected to be in strictly increasing order.
++ *
++ * @param name name of the argument.
++ * @param i index of the array element in the argument.
++ * @param previous the value during previous iteration.
++ * @param max the maximal value, exclusive.
++ * @param value the value to validate.
++ * @return {@code null} if the value is valid, otherwise the message to put in an exception.
++ */
++ static String validate(final String name, final int i, final int previous, final int max, final int value) {
++ if (value <= previous || value >= max) {
++ return Errors.format(Errors.Keys.ValueOutOfRange_4,
++ Strings.toIndexed("dimensions", i), previous + 1, max - 1, value);
++ }
++ return null;
++ }
++
/**
* Adds the specified range to the specified sequence.
*
diff --cc storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 0ca8604f43,0ca8604f43..48c8c07a0d
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@@ -34,6 -34,6 +34,7 @@@ import org.opengis.referencing.operatio
import org.apache.sis.internal.storage.ResourceOnFileSystem;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.internal.util.Strings;
++import org.apache.sis.internal.coverage.RangeArgument;
import org.apache.sis.internal.coverage.j2d.RasterFactory;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.grid.GridExtent;
@@@ -55,7 -55,7 +56,6 @@@ import org.apache.sis.util.CharSequence
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.internal.storage.MetadataBuilder;
--import org.apache.sis.internal.storage.RangeArgument;
import org.apache.sis.internal.storage.StoreResource;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/GridResourceWrapper.java
index e4d5c628a9,e4d5c628a9..7439c61d84
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/GridResourceWrapper.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/GridResourceWrapper.java
@@@ -37,6 -37,6 +37,10 @@@ import org.opengis.util.GenericName
* Wrappers can be used for delaying data loading, modifying the identifier, completing metadata, <i>etc</i>.
* The wrapped resource is created only when first needed.
*
++ * <p>The default implementation assumes that the wrapper only delays data loading,
++ * without making substantive changes to the data. If the wrapper changes the data,
++ * then {@link DerivedGridCoverageResource} subclass should be used instead.</p>
++ *
* @author Martin Desruisseaux (Geomatys)
* @version 1.4
* @since 1.1
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
index 5dd5de3c15,5dd5de3c15..32a1eda795
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
@@@ -27,6 -27,6 +27,7 @@@ import org.apache.sis.coverage.grid.Gri
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridRoundingMode;
import org.apache.sis.image.ImageProcessor;
++import org.apache.sis.internal.coverage.RangeArgument;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.storage.AbstractGridCoverageResource;
import org.apache.sis.storage.event.StoreListeners;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index 4310f768ae,4310f768ae..d6662be5af
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@@ -140,7 -140,7 +140,7 @@@ public final class Resources extends In
/**
* Cannot select a slice.
*/
-- public static final short CanNotSelectSlice = 81;
++ public static final short CanNotSelectSlice = 53;
/**
* Cannot save resources of type ‘{1}’ in a “{0}” store.
@@@ -217,11 -217,11 +217,6 @@@
*/
public static final short DuplicatedQueryProperty_3 = 54;
-- /**
-- * Sample dimension index {0} is duplicated.
-- */
-- public static final short DuplicatedSampleDimensionIndex_1 = 53;
--
/**
* Exception occurred in a listener for events of type ‘{0}’.
*/
@@@ -299,11 -299,11 +294,6 @@@
*/
public static final short InvalidExpression_2 = 60;
-- /**
-- * Sample dimension index {1} is invalid. Expected an index from 0 to {0} inclusive.
-- */
-- public static final short InvalidSampleDimensionIndex_2 = 52;
--
/**
* Loaded grid coverage between {1} – {2} and {3} – {4} from file “{0}” in {5} seconds.
*/
@@@ -338,7 -338,7 +328,7 @@@
/**
* Extent in dimension “{0}” should be a slice, but {1} cells were specified.
*/
-- public static final short NoSliceSpecified_2 = 80;
++ public static final short NoSliceSpecified_2 = 52;
/**
* No directory of resources found at “{0}”.
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index 0bbeef82a9,0bbeef82a9..86fc1eb64a
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@@ -51,7 -51,7 +51,6 @@@ DataStoreTimeZone = Tim
DirectoryContent_1 = Content of \u201c{0}\u201d directory.
DirectoryContentFormatName = Name of the format to use for reading or writing the directory content.
DuplicatedQueryProperty_3 = Query property \u201c{0}\u201d is duplicated at indices {1} and {2}.
--DuplicatedSampleDimensionIndex_1 = Sample dimension index {0} is duplicated.
ExcessiveHeaderSize_1 = Header in the \u201c{0}\u201d file is too large.
ExcessiveStringSize_3 = Character string in the \u201c{0}\u201d file is too long. The string has {2} characters while the limit is {1}.
FeatureAlreadyPresent_2 = A feature named \u201c{1}\u201d is already present in the \u201c{0}\u201d data store.
@@@ -66,7 -66,7 +65,6 @@@ IllegalOutputTypeForWriter_2 = Th
IncompatibleGridGeometry = All coverages must have the same grid geometry.
InconsistentNameComponents_2 = Components of the \u201c{1}\u201d name are inconsistent with those of the name previously binded in \u201c{0}\u201d data store.
InvalidExpression_2 = Invalid or unsupported \u201c{1}\u201d expression at index {0}.
--InvalidSampleDimensionIndex_2 = Sample dimension index {1} is invalid. Expected an index from 0 to {0} inclusive.
ExceptionInListener_1 = Exception occurred in a listener for events of type \u2018{0}\u2019.
LoadedGridCoverage_6 = Loaded grid coverage between {1} \u2013 {2} and {3} \u2013 {4} from file \u201c{0}\u201d in {5} seconds.
MarkNotSupported_1 = Marks are not supported on \u201c{0}\u201d stream.
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 9cb9b60745,9cb9b60745..41c240d899
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@@ -56,7 -56,7 +56,6 @@@ DataStoreTimeZone = Fus
DirectoryContent_1 = Contenu du r\u00e9pertoire \u00ab\u202f{0}\u202f\u00bb.
DirectoryContentFormatName = Nom du format ou de la source de donn\u00e9es \u00e0 utiliser pour lire ou \u00e9crire le contenu du r\u00e9pertoire.
DuplicatedQueryProperty_3 = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb de la requ\u00eate est dupliqu\u00e9e aux indices {1} et {2}.
--DuplicatedSampleDimensionIndex_1 = L\u2019index de dimension d\u2019\u00e9chantillonnage {0} est r\u00e9p\u00e9t\u00e9.
ExcessiveHeaderSize_1 = L\u2019en-t\u00eate dans le fichier \u00ab\u202f{0}\u202f\u00bb est trop grand.
ExcessiveStringSize_3 = La cha\u00eene de caract\u00e8res dans le fichier \u00ab\u202f{0}\u202f\u00bb est trop longue. La cha\u00eene fait {2} caract\u00e8res alors que la limite est {1}.
FeatureAlreadyPresent_2 = Une entit\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb est d\u00e9j\u00e0 pr\u00e9sente dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
@@@ -70,7 -70,7 +69,6 @@@ IllegalInputTypeForReader_2 = Le
IllegalOutputTypeForWriter_2 = L\u2019encodeur {0} n\u2019accepte pas des sorties de type \u2018{1}\u2019.
IncompatibleGridGeometry = Toutes les couvertures de donn\u00e9es doivent avoir la m\u00eame g\u00e9om\u00e9trie de grille.
InvalidExpression_2 = Expression \u00ab\u202f{1}\u202f\u00bb invalide ou non-support\u00e9e \u00e0 l\u2019index {0}.
--InvalidSampleDimensionIndex_2 = L\u2019index de dimension d\u2019\u00e9chantillonnage {1} est invalide. On attendait un index de 0 \u00e0 {0} inclusif.
InconsistentNameComponents_2 = Les \u00e9l\u00e9ments qui composent le nom \u00ab\u202f{1}\u202f\u00bb ne sont pas coh\u00e9rents avec ceux du nom qui avait \u00e9t\u00e9 pr\u00e9c\u00e9demment li\u00e9 dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
ExceptionInListener_1 = Une exception est survenue dans un capteur d\u2019\u00e9v\u00e9nements de type \u2018{0}\u2019.
LoadedGridCoverage_6 = Lecture d\u2019une couverture de donn\u00e9es entre {1} \u2013 {2} et {3} \u2013 {4} \u00e0 partir du fichier \u00ab\u202f{0}\u202f\u00bb en {5} secondes.
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index 468f2b3c6e,468f2b3c6e..86213d27b4
--- 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
@@@ -34,6 -34,6 +34,7 @@@ 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.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/internal/storage/esri/AsciiGridStore.java
index d9c1b684ce,d9c1b684ce..52c9e98c7c
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java
@@@ -36,7 -36,7 +36,7 @@@ import org.apache.sis.storage.StorageCo
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreClosedException;
import org.apache.sis.storage.DataStoreContentException;
--import org.apache.sis.internal.storage.RangeArgument;
++import org.apache.sis.internal.coverage.RangeArgument;
import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.storage.io.ChannelDataInput;
import org.apache.sis.measure.NumberRange;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
index fae34dca7e,fae34dca7e..d61a2ce50d
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
@@@ -48,7 -48,7 +48,7 @@@ import org.apache.sis.internal.storage.
import org.apache.sis.internal.storage.MetadataBuilder;
import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
--import org.apache.sis.internal.storage.RangeArgument;
++import org.apache.sis.internal.coverage.RangeArgument;
import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.internal.util.Numerics;
@@@ -419,7 -419,7 +419,7 @@@ abstract class RasterStore extends PRJD
*/
if (band == VISIBLE_BAND) {
if (isRGB) {
-- colorModel = ColorModelFactory.createRGB(sm);
++ colorModel = ColorModelFactory.createRGB(sm).get(); // Should not be empty.
} else {
try {
colorModel = readColorMap(dataType, (int) (maximum + 1), bands.length);
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterReader.java
index ca43a79194,ca43a79194..d7aa9b6cc8
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterReader.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterReader.java
@@@ -28,12 -28,12 +28,12 @@@ import org.apache.sis.image.DataType
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridDerivation;
--import org.apache.sis.internal.storage.RangeArgument;
++import org.apache.sis.internal.coverage.RangeArgument;
++import org.apache.sis.internal.coverage.j2d.RasterFactory;
++import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.storage.io.ChannelDataInput;
import org.apache.sis.internal.storage.io.HyperRectangleReader;
import org.apache.sis.internal.storage.io.Region;
--import org.apache.sis.internal.coverage.j2d.ImageUtilities;
--import org.apache.sis.internal.coverage.j2d.RasterFactory;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.util.ArraysExt;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java
index 60fca66fef,60fca66fef..5fd07fae18
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java
@@@ -35,8 -35,8 +35,8 @@@ import org.apache.sis.coverage.SampleDi
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
++import org.apache.sis.internal.coverage.RangeArgument;
import org.apache.sis.internal.storage.Resources;
--import org.apache.sis.internal.storage.RangeArgument;
import org.apache.sis.internal.storage.io.ChannelDataInput;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.storage.DataStoreException;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
index af3790db90,af3790db90..e0da7fc8e9
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
@@@ -42,8 -42,8 +42,8 @@@ import org.apache.sis.storage.DataStore
import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.storage.StoreResource;
--import org.apache.sis.internal.storage.RangeArgument;
import org.apache.sis.internal.storage.io.IOUtilities;
++import org.apache.sis.internal.coverage.RangeArgument;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.util.resources.Vocabulary;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractGridCoverageResource.java
index 2d50bc577d,2d50bc577d..06d0cc5867
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractGridCoverageResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractGridCoverageResource.java
@@@ -60,10 -60,10 +60,22 @@@ import org.apache.sis.internal.storage.
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.2
*/
public abstract class AbstractGridCoverageResource extends AbstractResource implements GridCoverageResource {
++ /**
++ * Creates a new resource, potentially as a child of another resource.
++ * The parent resource is typically, but not necessarily, an {@link Aggregate}.
++ *
++ * @param parent the parent resource, or {@code null} if none.
++ *
++ * @since 1.4
++ */
++ protected AbstractGridCoverageResource(final Resource parent) {
++ super(parent);
++ }
++
/**
* Creates a new resource which can send notifications to the given set of listeners.
* If {@code hidden} is {@code false} (the recommended value), then this resource will have its own set of
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractResource.java
index 1bbc59468f,1bbc59468f..2a2e38316d
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractResource.java
@@@ -54,7 -54,7 +54,7 @@@ import org.apache.sis.xml.NilReason
* Synchronization, when needed, uses {@link #getSynchronizationLock()}.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.2
*/
public abstract class AbstractResource implements Resource {
@@@ -73,6 -73,6 +73,24 @@@
*/
private volatile Metadata metadata;
++ /**
++ * Creates a new resource, potentially as a child of another resource.
++ * The parent resource is typically, but not necessarily, an {@link Aggregate}.
++ *
++ * @param parent the parent resource, or {@code null} if none.
++ *
++ * @since 1.4
++ */
++ protected AbstractResource(final Resource parent) {
++ StoreListeners parentListeners = null;
++ if (parent instanceof AbstractResource) {
++ parentListeners = ((AbstractResource) parent).listeners;
++ } else if (parent instanceof DataStore) {
++ parentListeners = ((DataStore) parent).listeners;
++ }
++ listeners = new StoreListeners(parentListeners, this);
++ }
++
/**
* Creates a new resource which can send notifications to the given set of listeners.
* If {@code hidden} is {@code false} (the recommended value), then this resource will have its own set of
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java
index 2bfc6007b6,2bfc6007b6..89f227d1d2
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java
@@@ -19,6 -19,6 +19,7 @@@ package org.apache.sis.storage
import java.util.List;
import java.util.Arrays;
import java.util.Objects;
++import java.util.function.Function;
import java.io.Serializable;
import java.math.RoundingMode;
import org.opengis.util.GenericName;
@@@ -30,8 -30,8 +31,12 @@@ import org.apache.sis.measure.Latitude
import org.apache.sis.measure.Longitude;
import org.apache.sis.measure.AngleFormat;
import org.apache.sis.coverage.SampleDimension;
++import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridRoundingMode;
++import org.apache.sis.coverage.grid.GridCoverageProcessor;
++import org.apache.sis.coverage.grid.DimensionalityReduction;
++import org.apache.sis.coverage.grid.IllegalGridGeometryException;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
@@@ -56,7 -56,7 +61,7 @@@
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.1
*/
public class CoverageQuery extends Query implements Cloneable, Serializable {
@@@ -71,6 -71,6 +76,12 @@@
*/
private GridGeometry domain;
++ /**
++ * The dimensionality reduction to apply on coverage domain, or {@code null} if none.
++ */
++ @SuppressWarnings("serial") // The target parameterized types are serializable.
++ private Function<GridGeometry, DimensionalityReduction> reduction;
++
/**
* 0-based indices of sample dimensions to read, or {@code null} for reading them all.
* This is the "projection" (not to be confused with map projection) in query terminology.
@@@ -103,6 -103,6 +114,10 @@@
* Note that the given envelope is approximate:
* Coverages may expand the envelope to an integer amount of tiles.
*
++ * <p>If a {@linkplain #setAxisSelection(Function) dimensionality reduction} is applied,
++ * the specified envelope can be either the full envelope (with all dimensions)
++ * or an envelope with reduced dimensions.</p>
++ *
* @param domain the approximate area of interest, or {@code null} if none.
*/
@Override
@@@ -118,6 -118,6 +133,10 @@@
* Sets the desired grid extent and resolution. The given domain is approximate:
* Coverages may use a different resolution and expand the envelope to an integer amount of tiles.
*
++ * <p>If a {@linkplain #setAxisSelection(Function) dimensionality reduction} is applied,
++ * the specified domain can be either the full domain (with all dimensions) or a domain
++ * with reduced dimensions.</p>
++ *
* @param domain desired grid extent and resolution, or {@code null} for reading the whole domain.
*/
public void setSelection(final GridGeometry domain) {
@@@ -128,10 -128,10 +147,10 @@@
* Returns the desired grid extent and resolution.
* This is the value set by the last call to {@link #setSelection(GridGeometry)}.
*
-- * <div class="note"><b>Note on terminology:</b>
-- * "selection" is the generic term used in queries for designating a subset of feature instances.
++ * <h4>Note on terminology</h4>
++ * "Selection" is the generic term used in queries for designating a subset of feature instances.
* In a grid coverage, feature instances are cells or pixels.
-- * So this concept maps to the <cite>coverage domain</cite>.</div>
++ * So this concept maps to the <cite>coverage domain</cite>.
*
* @return desired grid extent and resolution, or {@code null} for reading the whole domain.
*/
@@@ -139,6 -139,6 +158,70 @@@
return domain;
}
++ /**
++ * Requests dimensionality reduction by selecting or removing specified domain axes.
++ * The axes to keep or remove are specified by a {@link DimensionalityReduction} object,
++ * which works with indices of <em>grid extent</em> axes. It may be the same indices
++ * than the indices of the CRS axes which will be kept or removed, but not necessarily.
++ * It is the {@link Function} responsibility to map CRS dimensions to grid dimensions if desired.
++ *
++ * <p><b>Example 1:</b> automatically reduce a grid coverage dimensionality
++ * by removing all grid axes with an extent size of 1.</p>
++ *
++ * {@snippet lang="java" :
++ * query.setAxisSelection(DimensionalityReduction::reduce);
++ * }
++ *
++ * <p><b>Example 2:</b> take a two-dimensional slice by keeping the two first axes
++ * and selecting the median grid coordinate (0.5 ratio) in all other dimensions.</p>
++ *
++ * {@snippet lang="java" :
++ * query.setAxisSelection((domain) -> DimensionalityReduction.select2D(domain).withSliceByRatio(0.5));
++ * }
++ *
++ * @param reduction the function to apply for obtaining a dimensionality reduction from a grid coverage,
++ * or {@code null} if none.
++ *
++ * @see GridCoverageProcessor#selectGridDimensions(GridCoverage, int...)
++ * @see GridCoverageProcessor#removeGridDimensions(GridCoverage, int...)
++ * @see GridCoverageProcessor#reduceDimensionality(GridCoverage)
++ *
++ * @since 1.4
++ */
++ public void setAxisSelection(final Function<GridGeometry, DimensionalityReduction> reduction) {
++ this.reduction = reduction;
++ }
++
++ /**
++ * Returns the dimensionality reduction to apply on coverage domain.
++ * This is the value specified in the last call to {@link #setAxisSelection(Function)}.
++ *
++ * @return the function to apply for obtaining a dimensionality reduction from a grid coverage,
++ * or {@code null} if none.
++ *
++ * @since 1.4
++ */
++ public Function<GridGeometry, DimensionalityReduction> getAxisSelection() {
++ return reduction;
++ }
++
++ /**
++ * Returns the dimensionality reduction to apply on the specified resource.
++ *
++ * @param source the resource for which to create a subset.
++ * @return dimensionality reduction to apply, or {@code null} if none.
++ * @throws DataStoreException if an error occurred while fetching grid geometry.
++ * @throws IndexOutOfBoundsException if a grid axis index is out of bounds.
++ * @throws IllegalGridGeometryException if the dimensionality reduction cannot be applied on the grid geometry.
++ */
++ final DimensionalityReduction getAxisSelection(final GridCoverageResource source) throws DataStoreException {
++ if (reduction != null) {
++ DimensionalityReduction r = reduction.apply(source.getGridGeometry());
++ if (!r.isIdentity()) return r;
++ }
++ return null;
++ }
++
/**
* Sets the sample dimensions to read by their names.
*
@@@ -181,10 -181,10 +264,10 @@@
* Returns the indices of samples dimensions to read, or {@code null} if there is no filtering on range.
* If non-null, the returned array shall never be empty.
*
-- * <div class="note"><b>Note on terminology:</b>
-- * "projection" (not to be confused with map projection) is the generic term used in queries
++ * <h4>Note on terminology</h4>
++ * "Projection" (not to be confused with map projection) is the generic term used in queries
* for designating a subset of feature properties retained in each feature instances.
-- * In a coverage, this concept maps to the <cite>coverage range</cite>.</div>
++ * In a coverage, this concept maps to the <cite>coverage range</cite>.
*
* @return 0-based indices of sample dimensions to read, or {@code null} for reading them all.
*/
@@@ -193,21 -193,21 +276,24 @@@
}
/**
-- * Converts the sample dimension names to sample dimension indices.
++ * Returns the indices of sample dimensions to read, converting names to indices if needed.
* This conversion depends on the resource on which the query will be applied.
*
-- * @param source the resource for which to to the conversion.
++ * @param source the resource for which to create a subset.
++ * @return 0-based indices of sample dimensions to read. Caller shall not modify.
++ * @throws DataStoreException if an error occurred while fetching sample dimensions.
*/
-- private void namesToIndices(final GridCoverageResource source) throws DataStoreException {
-- if (rangeNames != null) {
++ final int[] getProjection(final GridCoverageResource source) throws DataStoreException {
++ int[] sourceRange = range;
++ if (sourceRange == null && rangeNames != null) {
final List<SampleDimension> sd = source.getSampleDimensions();
final int numBands = sd.size();
-- range = new int[rangeNames.length];
++ sourceRange = new int[rangeNames.length];
next: for (int i=0; i<rangeNames.length; i++) {
final String name = rangeNames[i];
for (int j=0; j<numBands; j++) {
if (name.equals(sd.get(j).getName().toString())) {
-- range[i] = j;
++ sourceRange[i] = j;
continue next;
}
}
@@@ -215,8 -215,8 +301,8 @@@
.orElseGet(() -> Vocabulary.formatInternational(Vocabulary.Keys.Unnamed));
throw new UnsupportedQueryException(Errors.format(Errors.Keys.PropertyNotFound_2, id, name));
}
-- rangeNames = null;
}
++ return sourceRange;
}
/**
@@@ -262,7 -262,7 +348,8 @@@
*
* @param source the coverage resource to filter.
* @return a view over the given coverage resource containing only the given domain and range.
-- * @throws DataStoreException if an error occurred during creation of the subset.
++ * @throws DataStoreException if an error occurred while fetching information from the source.
++ * @throws IllegalGridGeometryException if a dimensionality reduction was requested but cannot be applied.
*
* @see GridCoverageResource#subset(CoverageQuerty)
* @see FeatureQuery#execute(FeatureSet)
@@@ -271,9 -271,9 +358,7 @@@
*/
protected GridCoverageResource execute(final GridCoverageResource source) throws DataStoreException {
ArgumentChecks.ensureNonNull("source", source);
-- final CoverageQuery query = clone();
-- query.namesToIndices(source);
-- return new CoverageSubset(source, query);
++ return new CoverageSubset(null, source, this);
}
/**
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
index 9cf9bac9b7,9cf9bac9b7..6331fe84ca
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
@@@ -16,8 -16,8 +16,11 @@@
*/
package org.apache.sis.storage;
--import java.util.Arrays;
import java.util.List;
++import java.util.Arrays;
++import java.util.Optional;
++import java.util.stream.Collectors;
++import org.opengis.util.GenericName;
import org.opengis.metadata.Metadata;
import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.TransformException;
@@@ -27,7 -27,7 +30,10 @@@ import org.apache.sis.coverage.grid.Gri
import org.apache.sis.coverage.grid.GridDerivation;
import org.apache.sis.coverage.grid.GridRoundingMode;
import org.apache.sis.coverage.grid.GridClippingMode;
++import org.apache.sis.coverage.grid.DimensionalityReduction;
import org.apache.sis.coverage.grid.DisjointExtentException;
++import org.apache.sis.internal.coverage.RangeArgument;
++import org.apache.sis.internal.referencing.DirectPositionView;
import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.storage.MetadataBuilder;
import org.apache.sis.internal.storage.StoreUtilities;
@@@ -40,31 -40,31 +46,79 @@@ import org.apache.sis.internal.util.Unm
* arguments of {@link GridCoverageResource#read(GridGeometry, int...)} method.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.3
++ * @version 1.4
* @since 1.1
*/
final class CoverageSubset extends AbstractGridCoverageResource {
++ /**
++ * Name for this coverage subset, or {@code null} if none.
++ */
++ private final GenericName name;
++
/**
* The coverage resource instance which provides the data.
*/
private final GridCoverageResource source;
/**
-- * The domain and range to read from the {@linkplain #source} coverage.
++ * The domain of this resource, computed when first requested.
++ * Sometime but not always the same as {@link #areaOfInterest}.
++ * If {@link #reduction} is non-null, then this is the reduced grid geometry.
++ *
++ * @see #getGridGeometry()
++ */
++ private GridGeometry gridGeometry;
++
++ /**
++ * The domain specified in the query, potentially with dimensions added for a slice.
++ * The number of dimensions should be the same as {@linkplain #source} grid geometry.
++ * May be {@code null} if no area of interest has been specified.
++ */
++ private final GridGeometry areaOfInterest;
++
++ /**
++ * The dimensionality reduction to apply on the coverages, or {@code null} if none.
*/
-- private final CoverageQuery query;
++ private final DimensionalityReduction reduction;
++
++ /**
++ * Number of additional cells to read on each border of the source grid coverage.
++ */
++ private final int sourceDomainExpansion;
++
++ /**
++ * 0-based indices of sample dimensions to read, or {@code null} for reading them all.
++ */
++ private final int[] selectedRange;
/**
* Creates a new coverage resource by filtering the given coverage using the given query.
-- * This given query is stored as-is (it is not cloned neither optimized).
*
-- * @param source the coverage resource instances which provides the data.
-- * @param query the domain and range to read from the {@code source} coverage.
++ * @param name name for this coverage subset, or {@code null} if none.
++ * @param source the coverage resource instances which provides the data.
++ * @param query the domain and range to read from the {@code source} coverage.
++ * @throws DataStoreException if an error occurred while reading from the source resource.
*/
-- CoverageSubset(final GridCoverageResource source, final CoverageQuery query) {
-- super(source instanceof AbstractResource ? ((AbstractResource) source).listeners : null, false);
++ CoverageSubset(final GenericName name, final GridCoverageResource source, final CoverageQuery query) throws DataStoreException {
++ super(source);
++ this.name = name;
this.source = source;
-- this.query = query;
++ reduction = query.getAxisSelection(source);
++ GridGeometry selection = query.getSelection();
++ if (selection != null && reduction != null && reduction.isReduced(selection)) {
++ selection = reduction.reverse(selection);
++ }
++ areaOfInterest = selection;
++ selectedRange = query.getProjection(source);
++ sourceDomainExpansion = query.getSourceDomainExpansion();
++ }
++
++ /**
++ * Returns the name for this resource if specified.
++ */
++ @Override
++ public Optional<GenericName> getIdentifier() {
++ return Optional.ofNullable(name);
}
/**
@@@ -81,6 -81,6 +135,24 @@@
return builder.build();
}
++ /**
++ * Returns the preferred resolutions (in units of CRS axes) for read operations in this data store.
++ * In absence of dimensionality reduction, this is the same resolution than the source resource.
++ *
++ * @return preferred resolutions for read operations in this data store, or an empty array if none.
++ * @throws DataStoreException if an error occurred while reading definitions from the underlying data store.
++ */
++ @Override
++ public List<double[]> getResolutions() throws DataStoreException {
++ List<double[]> resolutions = source.getResolutions();
++ if (reduction != null) {
++ resolutions.stream().map((resolution) -> {
++ return reduction.apply(new DirectPositionView.Double(resolution)).getCoordinate();
++ }).collect(Collectors.toList()); // TODO: replace by Stream.toList() on JDK16.
++ }
++ return resolutions;
++ }
++
/**
* Returns the valid extent of grid coordinates clipped to the area specified in the query.
* It should be the geometry of the coverage that we get when invoking {@link #read read(…)}
@@@ -92,20 -92,20 +164,27 @@@
* or if the grid geometry cannot be clipped to the query area.
*/
@Override
-- public GridGeometry getGridGeometry() throws DataStoreException {
-- /*
-- * The grid should be fully contained in the resource specified at construction time,
-- * including margin expansion (if any), because data may not exist outside that area.
-- * This requirement is specified by `GridClippingMode.STRICT`.
-- */
-- return clip(source.getGridGeometry(), GridRoundingMode.NEAREST, GridClippingMode.STRICT);
++ public synchronized GridGeometry getGridGeometry() throws DataStoreException {
++ GridGeometry domain = gridGeometry;
++ if (domain == null) {
++ /*
++ * The grid should be fully contained in the resource specified at construction time,
++ * including margin expansion (if any), because data may not exist outside that area.
++ * This requirement is specified by `GridClippingMode.STRICT`.
++ */
++ domain = clip(source.getGridGeometry(), GridRoundingMode.NEAREST, GridClippingMode.STRICT);
++ if (reduction != null) {
++ domain = reduction.apply(domain);
++ }
++ gridGeometry = domain; // Set only on success.
++ }
++ return domain;
}
/**
* Clips the given domain to the area of interest specified by the query. If any grid geometry is null,
-- * the other one is returned. The {@code domain} argument should be the domain to read as specified to
-- * {@link #read(GridGeometry, int...)}, or the full {@code CoverageSubset} domain if no value were given
-- * to the {@code read(…)} method.
++ * the other one is returned. The {@code domain} argument should have the number of dimensions expected
++ * by the {@linkplain #source}, using {@link #reduction} if necessary for changing the dimensionality.
*
* @param domain the domain requested in a read operation, or {@code null}.
* @param rounding whether to clip to nearest box or an enclosing box.
@@@ -116,15 -116,15 +195,13 @@@
private GridGeometry clip(final GridGeometry domain, final GridRoundingMode rounding, final GridClippingMode clipping)
throws DataStoreException
{
-- final GridGeometry areaOfInterest = query.getSelection();
if (domain == null) return areaOfInterest;
if (areaOfInterest == null) return domain;
try {
final GridDerivation derivation = domain.derive().rounding(rounding).clipping(clipping);
-- final int expansion = query.getSourceDomainExpansion();
-- if (expansion != 0) {
++ if (sourceDomainExpansion != 0) {
final int[] margins = new int[domain.getDimension()];
-- Arrays.fill(margins, expansion);
++ Arrays.fill(margins, sourceDomainExpansion);
derivation.margin(margins);
}
return derivation.subgrid(areaOfInterest).build();
@@@ -152,17 -152,17 +229,16 @@@
@Override
public List<SampleDimension> getSampleDimensions() throws DataStoreException {
final List<SampleDimension> dimensions = source.getSampleDimensions();
-- final int[] range = query.getProjection();
-- if (range == null) {
++ if (selectedRange == null) {
return dimensions;
}
-- final SampleDimension[] subset = new SampleDimension[range.length];
-- for (int i=0; i<range.length; i++) {
-- final int j = range[i];
++ final SampleDimension[] subset = new SampleDimension[selectedRange.length];
++ for (int i=0; i < selectedRange.length; i++) {
++ final int j = selectedRange[i];
try {
subset[i] = dimensions.get(j);
} catch (IndexOutOfBoundsException e) {
-- throw new DataStoreException(invalidRange(dimensions.size(), j), e);
++ throw new DataStoreException(e);
}
}
return UnmodifiableArrayList.wrap(subset);
@@@ -190,8 -190,8 +266,10 @@@
* Loads a subset of the grid coverage represented by this resource.
* The domain to be read by the resource is computed as below:
* <ul>
-- * <li>If the query specifies a {@link CoverageQuery#getSelection() domain},
-- * the given domain is intersected with the query domain.</li>
++ * <li>If the query specifies a {@linkplain CoverageQuery#getAxisSelection() dimensionality reduction},
++ * the given domain is inflated to the number of dimensions of the source.</li>
++ * <li>If the query specifies an {@linkplain CoverageQuery#getSelection() area of interest},
++ * the given domain is intersected with the query selection.</li>
* <li>If the query specifies a {@linkplain CoverageQuery#getSourceDomainExpansion() domain expansion},
* the given domain is expanded by the amount specified in the query.</li>
* </ul>
@@@ -203,6 -203,6 +281,13 @@@
*/
@Override
public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException {
++ /*
++ * Need to convert to the full number of dimensions first, even if the domain is null,
++ * because this operation may return a domain with slice coordinates.
++ */
++ if (reduction != null) {
++ domain = reduction.reverse(domain);
++ }
if (domain == null) {
domain = source.getGridGeometry();
}
@@@ -211,32 -211,32 +296,21 @@@
* specified `domain` but inside the source domain, which may be larger.
*/
domain = clip(domain, GridRoundingMode.ENCLOSING, GridClippingMode.BORDER_EXPANSION);
-- final int[] qr = query.getProjection();
-- if (ranges == null) {
-- ranges = qr;
-- } else if (qr != null) {
-- final int[] sub = new int[ranges.length];
++ if (selectedRange != null) {
++ final var validation = RangeArgument.validate(selectedRange.length, ranges, listeners);
++ ranges = new int[validation.getNumBands()];
for (int i=0; i<ranges.length; i++) {
-- final int j = ranges[i];
-- if (j >= 0 && j < qr.length) {
-- sub[i] = qr[j];
-- } else {
-- throw new IllegalArgumentException(invalidRange(qr.length, j));
-- }
++ ranges[validation.getTargetIndex(i)] = selectedRange[validation.getSourceIndex(i)];
}
-- ranges = sub;
}
-- return source.read(domain, ranges);
-- }
--
-- /**
-- * Creates an exception message for an invalid range index.
-- *
-- * @param size number of sample dimensions in source coverage.
-- * @param index the index which is out of bounds.
-- */
-- private String invalidRange(final int size, final int index) {
-- return Resources.forLocale(listeners.getLocale()).getString(
-- Resources.Keys.InvalidSampleDimensionIndex_2, size - 1, index);
++ /*
++ * Read the coverage will all dimensions, then provide a view
++ * with a reduced number of dimensions if that was requested.
++ */
++ GridCoverage coverage = source.read(domain, ranges);
++ if (reduction != null) {
++ coverage = reduction.apply(coverage);
++ }
++ return coverage;
}
}
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
index d524735118,d524735118..caeb0b193d
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
@@@ -40,7 -40,7 +40,7 @@@ import org.apache.sis.util.ArraysExt
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.0
*/
public interface GridCoverageResource extends DataSet {
@@@ -171,8 -171,8 +171,10 @@@
*/
default GridCoverageResource subset(final Query query) throws UnsupportedQueryException, DataStoreException {
ArgumentChecks.ensureNonNull("query", query);
-- if (query instanceof CoverageQuery) {
++ if (query instanceof CoverageQuery) try {
return ((CoverageQuery) query).execute(this);
++ } catch (RuntimeException e) {
++ throw new UnsupportedQueryException(e);
} else {
throw new UnsupportedQueryException();
}
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
index 0000000000,0000000000..9720025fb9
new file mode 100644
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
@@@ -1,0 -1,0 +1,379 @@@
++/*
++ * 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.storage.aggregate;
++
++import java.util.List;
++import java.util.Arrays;
++import java.util.Optional;
++import java.awt.image.ColorModel;
++import org.opengis.util.GenericName;
++import org.opengis.metadata.Metadata;
++import org.apache.sis.coverage.SampleDimension;
++import org.apache.sis.coverage.grid.GridCoverage;
++import org.apache.sis.coverage.grid.GridGeometry;
++import org.apache.sis.coverage.grid.GridCoverageProcessor;
++import org.apache.sis.coverage.grid.IllegalGridGeometryException;
++import org.apache.sis.internal.coverage.MultiSourcesArgument;
++import org.apache.sis.internal.coverage.RangeArgument;
++import org.apache.sis.storage.Resource;
++import org.apache.sis.storage.GridCoverageResource;
++import org.apache.sis.storage.AbstractGridCoverageResource;
++import org.apache.sis.storage.RasterLoadingStrategy;
++import org.apache.sis.storage.DataStoreException;
++import org.apache.sis.internal.storage.MetadataBuilder;
++import org.apache.sis.internal.util.UnmodifiableArrayList;
++import org.apache.sis.util.collection.BackingStoreException;
++
++
++/**
++ * A resource whose range is the aggregation of the ranges of a sequence of resources.
++ * This class combines homogeneous {@link GridCoverageResource}s by "stacking" their bands.
++ * The grid geometry is typically the same for all resources, but some variations described below are allowed.
++ * The number of sample dimensions in the aggregated coverage is the sum of the number of sample dimensions in
++ * each individual resource, unless a subset of sample dimensions is specified.
++ *
++ * <h2>Restrictions</h2>
++ * <ul>
++ * <li>All resources shall use the same coordinate reference system (CRS).</li>
++ * <li>All resources shall have the same {@linkplain GridCoverageResource#getGridGeometry() domain}, except
++ * for the grid extent and the translation terms which can vary by integer amounts of grid cells.</li>
++ * <li>All grid extents shall intersect and the intersection area shall be non-empty.</li>
++ * <li>If coverage data are stored in {@link java.awt.image.RenderedImage} instances,
++ * then all images shall use the same data type.</li>
++ * </ul>
++ *
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ *
++ * @see GridCoverageProcessor#aggregateRanges(GridCoverage[], int[][], ColorModel)
++ *
++ * @since 1.4
++ */
++public class BandAggregateGridResource extends AbstractGridCoverageResource {
++ /**
++ * Identifier of this resource.
++ *
++ * @see #getIdentifier()
++ */
++ private final GenericName name;
++
++ /**
++ * The source grid coverage resources.
++ */
++ private final GridCoverageResource[] sources;
++
++ /**
++ * The grid geometry of this resources, computed from all sources.
++ * All sources shall have this grid geometry except for grid extent and translation terms.
++ *
++ * @see #getGridGeometry()
++ */
++ private final GridGeometry gridGeometry;
++
++ /**
++ * The resolutions, or {@code null} if not yet computed.
++ * Can be an empty array after computation.
++ * Shall be read-only after computation.
++ *
++ * @see #getResolutions()
++ */
++ private double[][] resolutions;
++
++ /**
++ * The union or a subset of the union of all sample dimensions in this combined resource.
++ * Should be an unmodifiable list.
++ *
++ * @see #getSampleDimensions()
++ */
++ private final List<SampleDimension> sampleDimensions;
++
++ /**
++ * The sample dimensions to use for each source coverage, in order.
++ * The length of this array is always equal to {@link #sources} array length.
++ * This array does not contain any {@code null} or empty element.
++ */
++ private final int[][] bandsPerSource;
++
++ /**
++ * The color model to apply on aggregated image, or {@code null} for default.
++ * If {@code null}, the color model will be inferred from the aggregated number
++ * of bands and the sample data type.
++ */
++ private final ColorModel colors;
++
++ /**
++ * The processor to use for creating grid coverages.
++ */
++ private final GridCoverageProcessor processor;
++
++ /**
++ * Creates a new aggregation of all sample dimensions of all specified grid coverage resources.
++ * The new resource has no name and no parent, and use a default processor with default color model.
++ *
++ * @param sources resources whose bands shall be aggregated, in order. At least one resource must be provided.
++ * @throws DataStoreException if an error occurred while fetching the grid geometry or sample dimensions from a resource.
++ * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
++ */
++ public BandAggregateGridResource(final GridCoverageResource... sources) throws DataStoreException {
++ this(null, null, sources, null, null, null);
++ }
++
++ /**
++ * Creates a new range aggregation of grid coverage resources.
++ * The {@linkplain #getSampleDimensions() list of sample dimensions} of the aggregated resource
++ * will be the concatenation of the lists of all sources, or a subset of this concatenation.
++ *
++ * <p>The {@code bandsPerSource} argument specifies the bands to select in each resource.
++ * That array can be {@code null} for selecting all bands in all resources,
++ * or may contain {@code null} elements for selecting all bands of the corresponding resource.
++ * An empty array element (i.e. zero band to select) discards the corresponding resource.
++ * In the latter case, the discarded element in the {@code sources} array may be {@code null}.</p>
++ *
++ * <h4>Restrictions</h4>
++ * All resources shall have compatible domain, defined as below:
++ *
++ * <ul>
++ * <li>Same CRS.</li>
++ * <li>Same <cite>grid to CRS</cite> transform except for translation terms.</li>
++ * <li>Translation terms that differ only by an integer amount of grid cells.</li>
++ * </ul>
++ *
++ * The intersection of the domain of all resources shall be non-empty,
++ * and all resources shall use the same data type in their rendered image.
++ *
++ * @param parent the parent resource, or {@code null} if none.
++ * @param name name of the combined grid coverage resource, or {@code null} if none.
++ * @param sources resources whose bands shall be aggregated, in order. At least one resource must be provided.
++ * @param bandsPerSource sample dimensions for each source. May be {@code null} or may contain {@code null} elements.
++ * @param processor the processor to use for creating grid coverages, or {@code null} for a default processor.
++ * @param colors the color model to apply on aggregated image, or {@code null} for inferring
++ * a default color model using aggregated number of bands and sample data type.
++ * @throws DataStoreException if an error occurred while fetching the grid geometry or sample dimensions from a resource.
++ * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
++ * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
++ */
++ public BandAggregateGridResource(final Resource parent, final GenericName name,
++ final GridCoverageResource[] sources, final int[][] bandsPerSource,
++ final GridCoverageProcessor processor, final ColorModel colors) throws DataStoreException
++ {
++ super(parent);
++ try {
++ final var aggregate = new MultiSourcesArgument<GridCoverageResource>(sources, bandsPerSource);
++ aggregate.validate(BandAggregateGridResource::range);
++ this.name = name;
++ this.sources = aggregate.sources();
++ this.gridGeometry = aggregate.domain(BandAggregateGridResource::domain);
++ this.sampleDimensions = List.copyOf(aggregate.ranges());
++ this.bandsPerSource = aggregate.bandsPerSource();
++ this.processor = (processor != null) ? processor : new GridCoverageProcessor();
++ this.colors = colors;
++ } catch (BackingStoreException e) {
++ throw e.unwrapOrRethrow(DataStoreException.class);
++ }
++ }
++
++ /**
++ * A getter method for grid geometry,
++ * wrapping the checked exception for enabling use with lambda functions.
++ */
++ private static GridGeometry domain(final GridCoverageResource source) {
++ try {
++ return source.getGridGeometry();
++ } catch (DataStoreException e) {
++ throw new BackingStoreException(e);
++ }
++ }
++
++ /**
++ * A getter method for sample dimensions,
++ * wrapping the checked exception for enabling use with lambda functions.
++ */
++ private static List<SampleDimension> range(final GridCoverageResource source) {
++ try {
++ return source.getSampleDimensions();
++ } catch (DataStoreException e) {
++ throw new BackingStoreException(e);
++ }
++ }
++
++ /**
++ * Returns the resource identifier if available.
++ * This is the name specified at construction time.
++ *
++ * @return an identifier for the band aggregation.
++ * @throws DataStoreException if the identifier can not be obtained.
++ */
++ @Override
++ public Optional<GenericName> getIdentifier() throws DataStoreException {
++ return Optional.ofNullable(name);
++ }
++
++ /**
++ * Returns the valid extent of grid coordinates and their conversions to real world coordinates.
++ * This grid geometry is inferred from the grid geometries of resources specified at construction time.
++ *
++ * @return extent of grid coordinates together with their mapping to "real world" coordinates.
++ * @throws DataStoreException if the grid geometry can not be obtained.
++ */
++ @Override
++ public GridGeometry getGridGeometry() throws DataStoreException {
++ return gridGeometry;
++ }
++
++ /**
++ * Returns the ranges of sample values together with the conversion from samples to real values.
++ * This is the union or a subset of the union of the ranges of all resources specified at construction time.
++ *
++ * @return ranges of sample values together with their mapping to "real values".
++ * @throws DataStoreException if the sample dimensions can not be obtained.
++ */
++ @Override
++ @SuppressWarnings("ReturnOfCollectionOrArrayField")
++ public List<SampleDimension> getSampleDimensions() throws DataStoreException {
++ return sampleDimensions;
++ }
++
++ /**
++ * Builds metadata the first time that {@code getMetadata()} is invoked.
++ * This method provides the metadata described in the
++ * {@linkplain AbstractGridCoverageResource#createMetadata() super-class method},
++ * with the addition of lineage information for each source.
++ *
++ * @return the newly created metadata, or {@code null} if unknown.
++ * @throws DataStoreException if an error occurred while reading metadata from a resource.
++ */
++ @Override
++ protected Metadata createMetadata() throws DataStoreException {
++ MetadataBuilder builder = new MetadataBuilder();
++ builder.addDefaultMetadata(this, listeners);
++ for (GridCoverageResource source : sources) {
++ builder.addSource(source.getMetadata());
++ }
++ return builder.build();
++ }
++
++ /**
++ * Returns the preferred resolutions (in units of CRS axes) for read operations in this resource.
++ * The default implementation returns only the resolutions that are declared by all sources.
++ *
++ * @return preferred resolutions for read operations in this resource, or an empty array if none.
++ * @throws DataStoreException if an error occurred while reading definitions from an underlying resource.
++ */
++ @Override
++ public synchronized List<double[]> getResolutions() throws DataStoreException {
++ if (resolutions == null) {
++ resolutions = ConcatenatedGridResource.commonResolutions(sources);
++ }
++ return UnmodifiableArrayList.wrap(resolutions);
++ }
++
++ /**
++ * Returns an indication about when the "physical" loading of raster data will happen.
++ * The default implementation returns the most conservative value of all sources.
++ *
++ * @return current raster data loading strategy for this resource.
++ * @throws DataStoreException if an error occurred while fetching data store configuration.
++ */
++ @Override
++ public synchronized RasterLoadingStrategy getLoadingStrategy() throws DataStoreException {
++ RasterLoadingStrategy conservative = RasterLoadingStrategy.AT_GET_TILE_TIME;
++ for (final GridCoverageResource source : sources) {
++ RasterLoadingStrategy s = source.getLoadingStrategy();
++ if (s.ordinal() < conservative.ordinal()) {
++ conservative = s;
++ if (s.ordinal() == 0) break;
++ }
++ }
++ return conservative;
++ }
++
++ /**
++ * Sets the preferred strategy about when to do the "physical" loading of raster data.
++ *
++ * @param strategy the desired strategy for loading raster data.
++ * @return {@code true} if the given strategy has been accepted by all sources.
++ * @throws DataStoreException if an error occurred while setting data store configuration.
++ */
++ @Override
++ public synchronized boolean setLoadingStrategy(final RasterLoadingStrategy strategy) throws DataStoreException {
++ boolean accepted = true;
++ for (final GridCoverageResource source : sources) {
++ accepted &= source.setLoadingStrategy(strategy);
++ }
++ return accepted;
++ }
++
++ /**
++ * Loads a subset of the grid coverage represented by this resource.
++ *
++ * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain.
++ * @param ranges 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
++ * @return the grid coverage for the specified domain and ranges.
++ * @throws IllegalArgumentException if the given domain or ranges are invalid.
++ * @throws DataStoreException if an error occurred while reading a grid coverage data.
++ */
++ @Override
++ public GridCoverage read(final GridGeometry domain, final int... ranges) throws DataStoreException {
++ final var validator = RangeArgument.validate(sampleDimensions.size(), ranges, listeners);
++ final int numBands = validator.getNumBands(); // Number of bands that are requested.
++ final var coverages = new GridCoverage[numBands]; // Grid coverages to aggregate. Duplicates are allowed.
++ final var coverageBands = new int[numBands][]; // Indices of `coverages` bands to aggregate.
++ final var bandsToLoad = new int[numBands]; // Buffer for bands to load with a single resource.
++ int numBandsToLoad = 0; // Number of valid indices in `bandsToLoad`.
++ int coverageCursor = 0; // Index in `bandsPerSource[#][]` for current source band.
++ int bandCursor = 0; // Index in `bandsPerSource[][#]` for current source band.
++ int cursorBase = 0; // First valid value of `cursorIndex` for current source.
++ int cursorIndex = 0; // Current band according numeration of this resource.
++ int pendingIndex = 0; // Next value of `i` where to write in `coverages` array.
++ for (int i=0; i <= numBands; i++) {
++ /*
++ * Iterate over the requested bands in increasing index order.
++ * The `source` value is guaranteed to be always increasing.
++ * This is not necessarily the same order than the one specified by the user.
++ * User order is taken in account later, with the call to `getTargetIndex(…)`.
++ */
++ final int source = (i != numBands) ? validator.getSourceIndex(i) : sampleDimensions.size();
++ bandCursor += source - cursorIndex;
++ int bandCursorMax;
++ int[] bandsForCurrentSource;
++ while (bandCursor >= (bandCursorMax = (bandsForCurrentSource = bandsPerSource[coverageCursor]).length)) {
++ /*
++ * If we enter in this block, the current band specified by user is on a different
++ * resources than the one in previous iteration. We need to execute the pending
++ * read operation before to move to the next resource.
++ */
++ if (numBandsToLoad != 0) {
++ final GridCoverage data = sources[coverageCursor].read(domain, Arrays.copyOf(bandsToLoad, numBandsToLoad));
++ numBandsToLoad = 0;
++ int b = 0;
++ do {
++ final int target = validator.getTargetIndex(pendingIndex);
++ coverageBands[target] = new int[] {b++};
++ coverages[target] = data;
++ } while (++pendingIndex != i);
++ }
++ bandCursor -= bandCursorMax;
++ cursorBase += bandCursorMax;
++ if (++coverageCursor >= bandsPerSource.length) break;
++ }
++ cursorIndex = source;
++ bandsToLoad[numBandsToLoad++] = bandsForCurrentSource[cursorIndex - cursorBase];
++ }
++ return processor.aggregateRanges(coverages, coverageBands, colors);
++ }
++}
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
index c6e68815e6,c6e68815e6..fb97d7f7c0
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
@@@ -38,7 -38,7 +38,7 @@@ import org.apache.sis.storage.RasterLoa
import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.internal.storage.MemoryGridResource;
import org.apache.sis.internal.storage.MetadataBuilder;
--import org.apache.sis.internal.storage.RangeArgument;
++import org.apache.sis.internal.coverage.RangeArgument;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.ArraysExt;
@@@ -50,7 -50,7 +50,7 @@@
* Instances of {@code ConcatenatedGridResource} are created by {@link CoverageAggregator}.
*
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.3
++ * @version 1.4
* @since 1.3
*/
final class ConcatenatedGridResource extends AbstractGridCoverageResource implements AggregatedResource {
@@@ -262,43 -262,43 +262,53 @@@
}
/**
-- * Returns the preferred resolutions (in units of CRS axes) for read operations in this data store.
-- * This method returns only the resolution that are declared by all coverages.
++ * Returns the preferred resolutions (in units of CRS axes) for read operations in this resource.
++ * This method returns only the resolutions that are declared by all coverages.
*
-- * @return preferred resolutions for read operations in this data store, or an empty array if none.
-- * @throws DataStoreException if an error occurred while reading definitions from the underlying data store.
++ * @return preferred resolutions for read operations in this resource, or an empty array if none.
++ * @throws DataStoreException if an error occurred while reading definitions from an underlying resource.
*/
@Override
public synchronized List<double[]> getResolutions() throws DataStoreException {
-- double[][] common = resolutions;
-- if (common == null) {
-- int count = 0;
-- for (final GridCoverageResource slice : slices) {
-- final double[][] sr = slice.getResolutions().toArray(double[][]::new);
-- if (sr != null) { // Should never be null, but we are paranoiac.
-- if (common == null) {
-- common = sr;
-- count = sr.length;
-- } else {
-- int retained = 0;
-- for (int i=0; i<count; i++) {
-- final double[] r = common[i];
-- for (int j=0; j<sr.length; j++) {
-- if (Arrays.equals(r, sr[j])) {
-- common[retained++] = r;
-- sr[j] = null;
-- break;
-- }
++ if (resolutions == null) {
++ resolutions = commonResolutions(slices);
++ }
++ return UnmodifiableArrayList.wrap(resolutions);
++ }
++
++ /**
++ * Computes resolutions common to all sources.
++ *
++ * @param sources the sources to use for computing the common resolution.
++ * @return the resolutions common to all given sources.
++ */
++ static double[][] commonResolutions(final GridCoverageResource[] sources) throws DataStoreException {
++ int count = 0;
++ double[][] resolutions = null;
++ for (final GridCoverageResource slice : sources) {
++ final double[][] sr = slice.getResolutions().toArray(double[][]::new);
++ if (sr != null) { // Should never be null, but we are paranoiac.
++ if (resolutions == null) {
++ resolutions = sr;
++ count = sr.length;
++ } else {
++ int retained = 0;
++ for (int i=0; i<count; i++) {
++ final double[] r = resolutions[i];
++ for (int j=0; j<sr.length; j++) {
++ if (Arrays.equals(r, sr[j])) {
++ resolutions[retained++] = r;
++ sr[j] = null;
++ break;
}
}
-- count = retained;
-- if (count == 0) break;
}
++ count = retained;
++ if (count == 0) break;
}
}
-- resolutions = common = ArraysExt.resize(common, count);
}
-- return UnmodifiableArrayList.wrap(common);
++ return ArraysExt.resize(resolutions, count);
}
/**
@@@ -335,7 -335,7 +345,7 @@@
* Slices are free to replace the given strategy by another one.
*
* @param strategy the desired strategy for loading raster data.
-- * @return {@code true} if the given strategy has been accepted by at least one slice.
++ * @return {@code true} if the given strategy has been accepted by all slices.
* @throws DataStoreException if an error occurred while setting data store configuration.
*/
@Override
diff --cc storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MemoryGridResourceTest.java
index 28683e4ae1,28683e4ae1..ef8a7fedaa
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MemoryGridResourceTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MemoryGridResourceTest.java
@@@ -25,7 -25,7 +25,6 @@@ import org.apache.sis.coverage.grid.Gri
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.referencing.crs.HardCodedCRS;
import org.apache.sis.coverage.grid.GridCoverage;
--import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestCase;
import org.junit.Test;
@@@ -39,7 -39,7 +38,6 @@@ import static org.apache.sis.test.Asser
* @version 1.2
* @since 1.1
*/
--@DependsOn(RangeArgumentTest.class)
public final class MemoryGridResourceTest extends TestCase {
/**
* Arbitrary size for the grid to test.
diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageSubsetTest.java
index 0000000000,0000000000..53c0abac97
new file mode 100644
--- /dev/null
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageSubsetTest.java
@@@ -1,0 -1,0 +1,130 @@@
++/*
++ * 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.storage;
++
++import java.util.List;
++import java.awt.image.DataBufferInt;
++import java.awt.image.RenderedImage;
++import org.opengis.referencing.crs.CoordinateReferenceSystem;
++import org.apache.sis.coverage.SampleDimension;
++import org.apache.sis.coverage.grid.BufferedGridCoverage;
++import org.apache.sis.coverage.grid.DimensionalityReduction;
++import org.apache.sis.coverage.grid.GridCoverage;
++import org.apache.sis.coverage.grid.GridExtent;
++import org.apache.sis.coverage.grid.GridGeometry;
++import org.apache.sis.coverage.grid.GridOrientation;
++import org.apache.sis.geometry.ImmutableEnvelope;
++import org.apache.sis.internal.storage.MemoryGridResource;
++import org.apache.sis.measure.Units;
++import org.apache.sis.referencing.crs.HardCodedCRS;
++import org.apache.sis.test.TestCase;
++import org.junit.Test;
++
++import static org.apache.sis.test.Assert.*;
++
++
++/**
++ * Tests {@link CoverageSubset}.
++ *
++ * @author Johann Sorel (Geomatys)
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ * @since 1.4
++ */
++public final class CoverageSubsetTest extends TestCase {
++ /**
++ * Size (in pixels) of the image to build for testing purpose.
++ */
++ private static final int WIDTH = 2, HEIGHT = 2;
++
++ /**
++ * Creates a new test case.
++ */
++ public CoverageSubsetTest() {
++ }
++
++ /**
++ * Creates a four-dimensional resource.
++ * Axes are (<var>longitude</var>, <var>latitude</var>, <var>height</var>, <var>time</var>).
++ */
++ private static GridCoverageResource createResource4D() {
++ final var lower = new double[] {0, 1, 2, 3}; // longitude, latitude, height, time.
++ final var upper = new double[] {4, 5, 6, 7};
++ final var extent = new GridExtent(null, null, new long[] {WIDTH, HEIGHT, 1, 1}, false);
++ final var region = new ImmutableEnvelope(lower, upper, HardCodedCRS.WGS84_4D);
++ final var domain = new GridGeometry(extent, region, GridOrientation.HOMOTHETY);
++ final var band = new SampleDimension.Builder().addQuantitative("101-based row-major order pixel number", 101, 105, 1, 0, Units.UNITY).build();
++ final var buffer = new DataBufferInt(values(), WIDTH * HEIGHT);
++ return new MemoryGridResource(null, new BufferedGridCoverage(domain, List.of(band), buffer));
++ }
++
++ /**
++ * Returns the sample values to store in the image.
++ * Array length shall be {@code WIDTH * HEIGHT}.
++ */
++ private static int[] values() {
++ return new int[] {101, 102, 103, 104};
++ }
++
++ /**
++ * Tests axis selection.
++ *
++ * @throws DataStoreException if an error occurred while creating or reading the reduced resource.
++ */
++ @Test
++ public void testAxisSelection() throws DataStoreException {
++ final GridCoverageResource data4D = createResource4D();
++ final CoverageQuery query = new CoverageQuery();
++
++ query.setAxisSelection(DimensionalityReduction::reduce);
++ verifyAxisSelection(data4D.subset(query), 2, HardCodedCRS.WGS84);
++
++ query.setAxisSelection((gg) -> DimensionalityReduction.select(gg, 0, 1, 3));
++ verifyAxisSelection(data4D.subset(query), 3, HardCodedCRS.WGS84_WITH_TIME);
++
++ query.setAxisSelection((gg) -> DimensionalityReduction.remove(gg, 3));
++ verifyAxisSelection(data4D.subset(query), 3, HardCodedCRS.WGS84_3D);
++ }
++
++ /**
++ * Verifies the result of an axis selection.
++ *
++ * @param reduced the result of axis selection.
++ * @param dimension expected number of dimensions.
++ * @param crs expected CRS.
++ * @throws DataStoreException if an error occurred while fetching information from the reduced coverage.
++ */
++ private static void verifyAxisSelection(final GridCoverageResource reduced,
++ final int dimension, final CoordinateReferenceSystem crs) throws DataStoreException
++ {
++ final GridGeometry domain = reduced.getGridGeometry();
++ assertEquals("Unexpected number of dimensions.", dimension, domain.getDimension());
++ assertEqualsIgnoreMetadata(crs, domain.getCoordinateReferenceSystem());
++
++ final GridCoverage loaded = reduced.read(null);
++ assertEquals(domain, loaded.getGridGeometry());
++
++ final RenderedImage image = loaded.render(null);
++ assertEquals(0, image.getMinX());
++ assertEquals(0, image.getMinY());
++ assertEquals(WIDTH, image.getWidth());
++ assertEquals(HEIGHT, image.getHeight());
++ final int[] values = image.getData().getPixels(0, 0, WIDTH, HEIGHT, (int[]) null);
++ assertArrayEquals(values(), values);
++ }
++}
diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
index 0000000000,0000000000..e4a1b8e434
new file mode 100644
--- /dev/null
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
@@@ -1,0 -1,0 +1,161 @@@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one or more
++ * contributor license agreements. See the NOTICE file distributed with
++ * this work for additional information regarding copyright ownership.
++ * The ASF licenses this file to You under the Apache License, Version 2.0
++ * (the "License"); you may not use this file except in compliance with
++ * the License. You may obtain a copy of the License at
++ *
++ * http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++package org.apache.sis.storage.aggregate;
++
++import java.util.List;
++import java.util.stream.Collectors;
++import java.util.stream.IntStream;
++import java.awt.image.DataBufferInt;
++import java.awt.image.RenderedImage;
++import org.opengis.util.LocalName;
++import org.opengis.referencing.datum.PixelInCell;
++import org.apache.sis.util.iso.Names;
++import org.apache.sis.referencing.operation.transform.MathTransforms;
++import org.apache.sis.coverage.SampleDimension;
++import org.apache.sis.coverage.grid.GridExtent;
++import org.apache.sis.coverage.grid.GridGeometry;
++import org.apache.sis.coverage.grid.GridCoverage;
++import org.apache.sis.coverage.grid.BufferedGridCoverage;
++import org.apache.sis.internal.storage.MemoryGridResource;
++import org.apache.sis.storage.DataStoreException;
++import org.apache.sis.storage.GridCoverageResource;
++import org.apache.sis.referencing.crs.HardCodedCRS;
++import org.apache.sis.test.TestCase;
++import org.junit.Test;
++
++import static org.junit.Assert.*;
++
++
++/**
++ * Tests {@link BandAggregateGridResource}.
++ *
++ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ * @since 1.4
++ */
++public final class BandAggregateGridResourceTest extends TestCase {
++ /**
++ * Width and height of images created for tests.
++ */
++ private static final int WIDTH = 2, HEIGHT = 3;
++
++ /**
++ * The grid geometry of all coverages created in this test.
++ */
++ private final GridGeometry domain;
++
++ /**
++ * Creates a new test case.
++ */
++ public BandAggregateGridResourceTest() {
++ domain = new GridGeometry(new GridExtent(WIDTH, HEIGHT), PixelInCell.CELL_CENTER,
++ MathTransforms.identity(2), HardCodedCRS.WGS84);
++ }
++
++ /**
++ * Tests aggregation of two resources having one band each.
++ * All source coverages share the same grid geometry.
++ *
++ * @throws DataStoreException if an error occurred while reading a resource.
++ */
++ @Test
++ public void aggregateBandsFromSingleBandSources() throws DataStoreException {
++ final GridCoverageResource first = singleValuePerBand(17);
++ final GridCoverageResource second = singleValuePerBand(23);
++ final var aggregation = new BandAggregateGridResource(first, second);
++
++ assertAllPixelsEqual(aggregation.read(null), 17, 23);
++ assertAllPixelsEqual(aggregation.read(null, 0), 17);
++ assertAllPixelsEqual(aggregation.read(null, 1), 23);
++ }
++
++ /**
++ * Tests aggregation of three resources having many bands.
++ * All source coverages share the same grid geometry.
++ *
++ * @throws DataStoreException if an error occurred while reading a resource.
++ */
++ @Test
++ public void aggregateBandsFromMultiBandSources() throws DataStoreException {
++ final GridCoverageResource firstAndSecondBands = singleValuePerBand(101, 102);
++ final GridCoverageResource thirdAndFourthBands = singleValuePerBand(103, 104);
++ final GridCoverageResource fifthAndSixthBands = singleValuePerBand(105, 106);
++
++ var aggregation = new BandAggregateGridResource(firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands);
++ aggregation.getIdentifier().ifPresent(name -> fail("No name provided at creation, but one was returned: " + name));
++ assertAllPixelsEqual(aggregation.read(null), 101, 102, 103, 104, 105, 106);
++ assertAllPixelsEqual(aggregation.read(null, 1, 2, 4, 5), 102, 103, 105, 106);
++ assertAllPixelsEqual(aggregation.read(null, 3, 4), 104, 105);
++ /*
++ * Test again, but specifying a subset of bands to the constructor.
++ * In addition, band order in one of the 3 coverages is modified.
++ */
++ final LocalName testName = Names.createLocalName(null, null, "test-name");
++ aggregation = new BandAggregateGridResource(null, testName,
++ new GridCoverageResource[] {firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands},
++ new int[][] {null, new int[] {1, 0}, new int[] {1}}, null, null);
++
++ assertEquals(testName, aggregation.getIdentifier().orElse(null));
++ assertAllPixelsEqual(aggregation.read(null), 101, 102, 104, 103, 106);
++ assertAllPixelsEqual(aggregation.read(null, 2, 4), 104, 106);
++ }
++
++ /**
++ * Creates a new grid coverage resource with bands having the given values.
++ * The length of the {@code bandValues} array is the number of bands to create.
++ * In a given band <var>b</var>, all pixels have the {@code bandValues[b]}.
++ *
++ * @param bandValues sample values for all pixels.
++ * @return a coverage with an image where all pixels have the specified sample values.
++ */
++ private GridCoverageResource singleValuePerBand(final int... bandValues) {
++ final var builder = new SampleDimension.Builder();
++ final List<SampleDimension> samples = IntStream.of(bandValues)
++ .mapToObj(b -> builder.setName("Band with value " + b).build())
++ .collect(Collectors.toList());
++
++ final int numBands = bandValues.length;
++ final int[] data = new int[WIDTH * HEIGHT * numBands];
++ for (int i=0; i < data.length; i += numBands) {
++ System.arraycopy(bandValues, 0, data, i, numBands);
++ }
++ final var values = new DataBufferInt(data, data.length);
++ return new MemoryGridResource(null, new BufferedGridCoverage(domain, samples, values));
++ }
++
++ /**
++ * Asserts that all pixels in the rendered image have the expected value.
++ *
++ * @param coverage the coverage for which to verify pixel values.
++ * @param pixel the expected pixel value.
++ */
++ private static void assertAllPixelsEqual(final GridCoverage coverage, final int... pixel) {
++ final int numBands = pixel.length;
++ final RenderedImage rendering = coverage.render(null);
++ assertEquals("minX", 0, rendering.getMinX());
++ assertEquals("minY", 0, rendering.getMinY());
++ assertEquals("width", WIDTH, rendering.getWidth());
++ assertEquals("height", HEIGHT, rendering.getHeight());
++ assertEquals("numBands", numBands, rendering.getSampleModel().getNumBands());
++ final int[] expected = new int[WIDTH * HEIGHT * numBands];
++ for (int i=0; i < expected.length; i += numBands) {
++ System.arraycopy(pixel, 0, expected, i, numBands);
++ }
++ assertArrayEquals(expected, rendering.getData().getPixels(0, 0, WIDTH, HEIGHT, (int[]) null));
++ }
++}
diff --cc storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
index e84a691f07,9171d1e38d..c60eee4a74
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
@@@ -42,13 -42,14 +42,13 @@@ import org.junit.BeforeClass
org.apache.sis.internal.storage.io.FileCacheByteChannelTest.class,
org.apache.sis.internal.storage.io.InputStreamArrayGetterTest.class,
org.apache.sis.internal.storage.MetadataBuilderTest.class,
-- org.apache.sis.internal.storage.RangeArgumentTest.class,
org.apache.sis.internal.storage.MemoryGridResourceTest.class,
- org.apache.sis.internal.storage.ConvertedCoverageResourceTest.class,
org.apache.sis.storage.FeatureNamingTest.class,
org.apache.sis.storage.ProbeResultTest.class,
org.apache.sis.storage.StorageConnectorTest.class,
org.apache.sis.storage.DataStoreProviderTest.class,
org.apache.sis.storage.event.StoreListenersTest.class,
++ org.apache.sis.storage.CoverageSubsetTest.class,
org.apache.sis.storage.CoverageQueryTest.class,
org.apache.sis.storage.FeatureQueryTest.class,
org.apache.sis.internal.storage.xml.MimeTypeDetectorTest.class,
@@@ -69,7 -70,8 +69,8 @@@
org.apache.sis.storage.aggregate.JoinFeatureSetTest.class,
org.apache.sis.storage.aggregate.ConcatenatedFeatureSetTest.class,
org.apache.sis.storage.aggregate.CoverageAggregatorTest.class,
- org.apache.sis.storage.DataStoresTest.class,
- org.apache.sis.storage.ResourceProcessorTest.class
++ org.apache.sis.storage.aggregate.BandAggregateGridResourceTest.class,
+ org.apache.sis.storage.DataStoresTest.class
})
public final class StorageTestSuite extends TestSuite {
/**
[sis] 01/02: Minor documentation fixes in comments.
Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 0e0a37dc5fe621c36fa3c43e55fe11e79310ffb2
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Mar 5 15:54:43 2023 +0100
Minor documentation fixes in comments.
---
.../src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java | 2 +-
.../sis/internal/referencing/provider/DatumShiftGridLoader.java | 4 ++--
.../src/main/java/org/apache/sis/internal/system/Configuration.java | 2 +-
core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java | 2 +-
.../src/test/java/org/apache/sis/measure/UnitFormatTest.java | 2 +-
core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java | 2 +-
6 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java b/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java
index 66ada480a6..a93f3ba346 100644
--- a/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java
+++ b/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java
@@ -44,7 +44,7 @@ final class CachedByteChannel extends FileCacheByteChannel {
* The connection will be opened when first needed.
*
* @param path path to the S3 file to open.
- * @throws IOException if the temporary file can not be created.
+ * @throws IOException if the temporary file cannot be created.
*/
CachedByteChannel(final KeyPath path) throws IOException {
super("S3-");
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
index 8f7e7919b2..edcbf5fa07 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
@@ -154,9 +154,9 @@ abstract class DatumShiftGridLoader {
*
* @param path the URI to make absolute.
* @return an absolute (if possible) URI to the data.
- * @throws NoSuchFileException if the path can not be made absolute.
+ * @throws NoSuchFileException if the path cannot be made absolute.
* This exception is necessary for letting the caller know that the coordinate operation is
- * probably valid but can not be constructed because an optional configuration is missing.
+ * probably valid but cannot be constructed because an optional configuration is missing.
* It is typically because the {@code SIS_DATA} environment variable has not been set.
*/
static URI toAbsolutePath(final URI path) throws NoSuchFileException {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Configuration.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Configuration.java
index 5c30cf939c..936d2d0162 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Configuration.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Configuration.java
@@ -70,7 +70,7 @@ public @interface Configuration {
/**
* The configuration is modifiable through a public method
* that must be invoked on a particular instance of a class.
- * This configuration can not be easily moved to {@link #GLOBAL}
+ * This configuration cannot be easily moved to {@link #GLOBAL}
* because it would require to decide on which instance to apply.
*/
INSTANCE,
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
index e8f9c99db6..5b90beefd3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
@@ -277,7 +277,7 @@ final class SystemUnit<Q extends Quantity<Q>> extends AbstractUnit<Q> implements
}
/**
- * Casts this unit to a parameterized unit of specified nature or throw a {@code ClassCastException}
+ * Casts this unit to a parameterized unit of specified nature or throws a {@code ClassCastException}
* if the dimension of the specified quantity and this unit's dimension do not match.
*
* @param <T> the type of the quantity measured by the unit.
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
index 82b8d984c3..22dcbdd565 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
@@ -133,7 +133,7 @@ public final class UnitFormatTest extends TestCase {
}
/**
- * Verifies one of the constants declared in the {@link Unit} class.
+ * Verifies one of the constants declared in the {@link Units} class.
*
* @param declared a map from which to remove the {@code field} value, for verifying that we didn't forgot an element.
* @param field the name of the constant to be verified.
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java
index 0cfe6162fd..9bcf8fe68f 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java
@@ -31,7 +31,7 @@ import static org.apache.sis.test.Assert.*;
/**
- * Test conversions using the units declared in {@link Units}.
+ * Test parsing, formatting and other operations on the units declared in {@link Units}.
*
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)