This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sis.git
commit 0865c08dfb62c88d7d7b40a3a6c8144f6b981b7f Merge: 7d5dfd074e 33688738c6 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Apr 16 19:42:02 2023 +0200 Merge branch 'geoapi-3.1', omitting `CopyVisitor` internal class. .../apache/sis/gui/coverage/CoverageCanvas.java | 59 +- .../apache/sis/gui/coverage/CoverageControls.java | 5 +- .../apache/sis/gui/coverage/CoverageStyling.java | 120 ++- .../org/apache/sis/gui/map/ValuesFormatter.java | 7 +- .../apache/sis/internal/gui/ImageConverter.java | 13 +- .../apache/sis/internal/gui/control/ColorCell.java | 33 +- .../internal/gui/control/ColorColumnHandler.java | 31 +- .../apache/sis/internal/gui/control/ColorRamp.java | 51 +- .../sis/internal/gui/control/ValueColorMapper.java | 12 - .../sis/internal/gui/control/package-info.java | 2 +- .../sis/gui/coverage/CoverageStylingApp.java | 5 +- .../apache/sis/cloud/aws/s3/CachedByteChannel.java | 2 +- .../org/apache/sis/coverage/BandedCoverage.java | 9 +- .../java/org/apache/sis/coverage/Category.java | 4 +- .../org/apache/sis/coverage/SampleDimension.java | 109 ++- .../coverage/grid/BandAggregateGridCoverage.java | 323 +++++++ .../sis/coverage/grid/BufferedGridCoverage.java | 22 +- .../sis/coverage/grid/ConvertedGridCoverage.java | 2 +- .../coverage/grid/CoordinateOperationFinder.java | 5 +- .../apache/sis/coverage/grid/DefaultEvaluator.java | 12 +- .../sis/coverage/grid/DerivedGridCoverage.java | 27 +- .../apache/sis/coverage/grid/DimensionReducer.java | 5 +- .../sis/coverage/grid/DimensionalityReduction.java | 988 +++++++++++++++++++++ .../sis/coverage/grid/DisjointExtentException.java | 9 +- .../coverage/grid/FractionalGridCoordinates.java | 6 +- .../sis/coverage/grid/GridCoordinatesView.java | 4 +- .../org/apache/sis/coverage/grid/GridCoverage.java | 84 +- .../apache/sis/coverage/grid/GridCoverage2D.java | 2 + .../sis/coverage/grid/GridCoverageBuilder.java | 26 +- .../sis/coverage/grid/GridCoverageProcessor.java | 451 +++++++++- .../org/apache/sis/coverage/grid/GridExtent.java | 261 +++--- .../org/apache/sis/coverage/grid/GridGeometry.java | 10 +- .../apache/sis/coverage/grid/GridOrientation.java | 2 +- .../apache/sis/coverage/grid/ImageRenderer.java | 103 ++- .../sis/coverage/grid/ReducedGridCoverage.java | 160 ++++ .../sis/coverage/grid/ResampledGridCoverage.java | 3 +- .../apache/sis/coverage/grid/SliceGeometry.java | 25 +- .../java/org/apache/sis/filter/LogicalFilter.java | 5 +- .../java/org/apache/sis/image/AnnotatedImage.java | 12 +- .../org/apache/sis/image/BandAggregateImage.java | 338 +++++++ .../java/org/apache/sis/image/BandSelectImage.java | 152 +++- .../org/apache/sis/image/BandSharedRaster.java | 181 ++++ .../java/org/apache/sis/image/BandSharing.java | 377 ++++++++ .../apache/sis/image/BandedSampleConverter.java | 180 ++-- .../main/java/org/apache/sis/image/Colorizer.java | 348 ++++++++ .../java/org/apache/sis/image/ComputedImage.java | 88 +- .../java/org/apache/sis/image/ImageAdapter.java | 6 +- .../java/org/apache/sis/image/ImageProcessor.java | 408 ++++++++- .../java/org/apache/sis/image/Interpolation.java | 6 +- .../main/java/org/apache/sis/image/MaskImage.java | 2 +- .../org/apache/sis/image/MultiSourceImage.java | 148 +++ .../org/apache/sis/image/MultiSourceLayout.java | 414 +++++++++ .../org/apache/sis/image/MultiSourcePrefetch.java | 178 ++++ .../java/org/apache/sis/image/PlanarImage.java | 95 +- .../java/org/apache/sis/image/RecoloredImage.java | 47 +- .../java/org/apache/sis/image/ResampledImage.java | 10 +- .../org/apache/sis/image/SourceAlignedImage.java | 8 +- .../main/java/org/apache/sis/image/Transferer.java | 55 +- .../java/org/apache/sis/image/UserProperties.java | 124 +++ .../java/org/apache/sis/image/Visualization.java | 244 +++-- .../apache/sis/image/WritableComputedImage.java | 177 ++++ .../java/org/apache/sis/index/tree/PointTree.java | 2 +- .../sis/internal/coverage/CommonDomainFinder.java | 385 ++++++++ .../sis/internal/coverage/MultiSourceArgument.java | 623 +++++++++++++ .../sis/internal/coverage}/RangeArgument.java | 46 +- .../sis/internal/coverage/SampleDimensions.java | 56 +- .../j2d/{Colorizer.java => ColorModelBuilder.java} | 256 ++++-- .../internal/coverage/j2d/ColorModelFactory.java | 336 ++++--- .../sis/internal/coverage/j2d/ColorModelType.java | 2 +- .../sis/internal/coverage/j2d/ColorsForRange.java | 166 +++- .../sis/internal/coverage/j2d/ImageLayout.java | 34 +- .../sis/internal/coverage/j2d/ImageUtilities.java | 78 +- .../coverage/j2d/MultiBandsIndexColorModel.java | 23 +- .../sis/internal/coverage/j2d/ObservableImage.java | 289 ++++++ .../sis/internal/coverage/j2d/RasterFactory.java | 34 +- .../internal/coverage/j2d/SampleModelFactory.java | 2 +- .../internal/coverage/j2d/ScaledColorModel.java | 15 +- .../internal/coverage/j2d/ScaledColorSpace.java | 31 +- .../internal/coverage/j2d/WritableTiledImage.java | 8 +- .../sis/internal/coverage/j2d/WriteSupport.java | 100 --- .../apache/sis/internal/coverage/package-info.java | 2 +- .../org/apache/sis/internal/feature/Resources.java | 40 + .../sis/internal/feature/Resources.properties | 8 + .../sis/internal/feature/Resources_fr.properties | 8 + .../apache/sis/internal/filter/package-info.java | 2 +- .../sis/internal/filter/sqlmm/SpatialFunction.java | 23 +- .../grid/BandAggregateGridCoverageTest.java | 183 ++++ .../coverage/grid/ConvertedGridCoverageTest.java | 24 +- .../coverage/grid/DimensionalityReductionTest.java | 194 ++++ .../apache/sis/coverage/grid/GridGeometryTest.java | 40 +- .../apache/sis/image/BandAggregateImageTest.java | 536 +++++++++++ .../org/apache/sis/image/BandSelectImageTest.java | 79 +- .../org/apache/sis/image/ImageProcessorTest.java | 62 +- .../apache/sis/image/StatisticsCalculatorTest.java | 2 +- .../java/org/apache/sis/image/TiledImageMock.java | 66 +- .../sis/internal/coverage}/RangeArgumentTest.java | 18 +- ...lorizerTest.java => ColorModelBuilderTest.java} | 20 +- .../apache/sis/test/suite/FeatureTestSuite.java | 8 +- .../apache/sis/internal/metadata/sql/Dialect.java | 31 +- .../org/apache/sis/util/iso/DefaultScopedName.java | 2 +- .../java/org/apache/sis/test/sql/TestDatabase.java | 24 +- .../sis/internal/map/coverage/RenderingData.java | 26 +- .../java/org/apache/sis/geometry/Envelopes.java | 2 +- .../referencing/provider/DatumShiftGridLoader.java | 4 +- .../apache/sis/parameter/ParameterValueList.java | 2 +- .../main/java/org/apache/sis/referencing/CRS.java | 40 +- .../referencing/factory/sql/AuthorityCodes.java | 2 +- .../operation/DefaultPassThroughOperation.java | 141 +-- .../operation/builder/LinearTransformBuilder.java | 2 +- .../referencing/operation/projection/Mercator.java | 4 +- .../operation/transform/MathTransforms.java | 37 + .../operation/transform/PassThroughTransform.java | 127 ++- .../operation/transform/TransformSeparator.java | 55 +- .../transform/PassThroughTransformTest.java | 65 +- .../apache/sis/internal/system/Configuration.java | 2 +- .../org/apache/sis/internal/util/Numerics.java | 13 + .../java/org/apache/sis/measure/NumberRange.java | 7 +- .../main/java/org/apache/sis/measure/Range.java | 23 +- .../java/org/apache/sis/measure/SystemUnit.java | 2 +- .../java/org/apache/sis/util/ArgumentChecks.java | 112 ++- .../main/java/org/apache/sis/util/ArraysExt.java | 140 +-- .../src/main/java/org/apache/sis/util/Version.java | 2 +- .../sis/util/collection/WeakValueHashMap.java | 89 +- .../java/org/apache/sis/util/package-info.java | 2 +- .../org/apache/sis/util/resources/Vocabulary.java | 5 + .../sis/util/resources/Vocabulary.properties | 1 + .../sis/util/resources/Vocabulary_fr.properties | 1 + .../java/org/apache/sis/measure/RangeTest.java | 17 +- .../org/apache/sis/measure/UnitFormatTest.java | 2 +- .../java/org/apache/sis/measure/UnitsTest.java | 2 +- .../org/apache/sis/util/ArgumentChecksTest.java | 6 +- .../java/org/apache/sis/util/ArraysExtTest.java | 32 +- .../apache/sis/storage/geotiff/GeoCodesTest.java | 1 - .../org/apache/sis/internal/netcdf/Convention.java | 3 +- .../org/apache/sis/internal/netcdf/Raster.java | 11 +- .../apache/sis/internal/netcdf/RasterResource.java | 6 +- .../apache/sis/internal/sql/feature/Database.java | 31 +- .../sis/internal/sql/feature/ValueGetter.java | 139 ++- .../sis/internal/sql/feature/package-info.java | 2 +- .../apache/sis/internal/sql/postgis/Postgres.java | 9 +- .../sis/internal/sql/postgis/RasterReader.java | 3 +- .../sis/internal/sql/postgis/package-info.java | 2 +- .../sql/feature/TemporalValueGetterTest.java | 238 +++++ .../org/apache/sis/storage/sql/SQLStoreTest.java | 104 +-- .../apache/sis/storage/sql/TestOnAllDatabases.java | 99 +++ .../org/apache/sis/test/suite/SQLTestSuite.java | 1 + .../sis/internal/storage/GridResourceWrapper.java | 4 + .../sis/internal/storage/MemoryFeatureSet.java | 12 +- .../sis/internal/storage/MemoryGridResource.java | 44 +- .../org/apache/sis/internal/storage/Resources.java | 14 +- .../sis/internal/storage/Resources.properties | 2 - .../sis/internal/storage/Resources_fr.properties | 2 - .../sis/internal/storage/TiledGridCoverage.java | 2 +- .../sis/internal/storage/TiledGridResource.java | 4 +- .../sis/internal/storage/esri/AsciiGridStore.java | 2 +- .../sis/internal/storage/esri/RasterStore.java | 18 +- .../sis/internal/storage/esri/RawRasterReader.java | 6 +- .../sis/internal/storage/esri/RawRasterStore.java | 2 +- .../apache/sis/internal/storage/folder/Store.java | 2 +- .../internal/storage/image/WorldFileResource.java | 2 +- .../sis/internal/storage/io/IOUtilities.java | 6 +- .../org/apache/sis/storage/AbstractFeatureSet.java | 14 +- .../sis/storage/AbstractGridCoverageResource.java | 14 +- .../org/apache/sis/storage/AbstractResource.java | 20 +- .../java/org/apache/sis/storage/CoverageQuery.java | 123 ++- .../org/apache/sis/storage/CoverageSubset.java | 186 ++-- .../java/org/apache/sis/storage/FeatureSubset.java | 4 +- .../apache/sis/storage/GridCoverageResource.java | 6 +- .../org/apache/sis/storage/StorageConnector.java | 44 +- .../storage/aggregate/AggregatedFeatureSet.java | 13 +- .../sis/storage/aggregate/AggregatedResource.java | 22 +- .../aggregate/BandAggregateGridResource.java | 450 ++++++++++ .../storage/aggregate/ConcatenatedFeatureSet.java | 8 +- .../aggregate/ConcatenatedGridCoverage.java | 2 +- .../aggregate/ConcatenatedGridResource.java | 99 ++- .../sis/storage/aggregate/CoverageAggregator.java | 252 +++++- .../apache/sis/storage/aggregate/GridSlice.java | 79 +- .../sis/storage/aggregate/GridSliceLocator.java | 3 +- .../org/apache/sis/storage/aggregate/Group.java | 4 +- .../sis/storage/aggregate/GroupAggregate.java | 33 +- .../apache/sis/storage/aggregate/GroupByCRS.java | 4 +- .../sis/storage/aggregate/GroupBySample.java | 8 +- .../sis/storage/aggregate/GroupByTransform.java | 8 +- .../sis/storage/aggregate/JoinFeatureSet.java | 10 +- .../sis/storage/aggregate/MergeStrategy.java | 7 +- .../internal/storage/MemoryGridResourceTest.java | 2 - .../org/apache/sis/storage/CoverageSubsetTest.java | 130 +++ .../aggregate/BandAggregateGridResourceTest.java | 228 +++++ .../storage/aggregate/CoverageAggregatorTest.java | 6 +- ...AggregatorTest.java => OpaqueGridResource.java} | 35 +- .../apache/sis/test/suite/StorageTestSuite.java | 3 +- 191 files changed, 11834 insertions(+), 2172 deletions(-) diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java index bb68262ecf,f0a24e8c9e..f563a8e813 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java @@@ -62,10 -62,9 +62,9 @@@ import org.apache.sis.util.Debug * <tr><td>[10…210]</td> <td>Temperature to be converted into Celsius degrees through a linear equation</td></tr> * </table> * In this example, sample values in range [10…210] define a quantitative category, while all others categories are qualitative. - * </div> * * <h2>Relationship with metadata</h2> - * This class provides the same information than ISO 19115 {@link org.opengis.metadata.content.SampleDimension}, + * This class provides the same information than ISO 19115 {@code org.opengis.metadata.content.SampleDimension}, * but organized in a different way. The use of the same name may seem a risk, but those two types are typically * not used at the same time. * diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java index 0000000000,517edb4390..09b7dcbf0d mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java @@@ -1,0 -1,989 +1,988 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.sis.coverage.grid; + + import java.util.Map; + import java.util.HashMap; + import java.util.Arrays; + import java.util.BitSet; + import java.util.function.UnaryOperator; + import java.io.Serializable; + import org.opengis.util.FactoryException; + import org.opengis.geometry.DirectPosition; + import org.opengis.geometry.MismatchedDimensionException; + import org.opengis.referencing.crs.CoordinateReferenceSystem; + import org.opengis.referencing.operation.MathTransformFactory; + import org.opengis.referencing.operation.MathTransform; + import org.opengis.referencing.datum.PixelInCell; + import org.apache.sis.util.Utilities; + import org.apache.sis.util.ArraysExt; + import org.apache.sis.util.ArgumentChecks; + import org.apache.sis.util.ComparisonMode; + import org.apache.sis.internal.util.Numerics; + import org.apache.sis.internal.util.ArgumentCheckByAssertion; + import org.apache.sis.internal.feature.Resources; + import org.apache.sis.geometry.ImmutableEnvelope; + import org.apache.sis.coverage.SubspaceNotSpecifiedException; + import org.apache.sis.geometry.GeneralDirectPosition; + import org.apache.sis.referencing.operation.transform.MathTransforms; + import org.apache.sis.referencing.operation.transform.TransformSeparator; + import org.apache.sis.referencing.operation.transform.PassThroughTransform; + import org.apache.sis.referencing.CRS; + + // Branch-dependent imports -import org.opengis.coverage.PointOutsideCoverageException; ++import org.apache.sis.coverage.PointOutsideCoverageException; + + + /** + * Description about how to reduce the number of dimensions of the domain of a grid coverage. + * This is a reduction in the number of dimensions of the grid extent, which usually implies + * a reduction in the number of dimensions of the CRS but not necessarily at the same indices + * (the relationship between grid dimensions and CRS dimensions is not necessarily straightforward). + * The sample dimensions (coverage range) are unmodified. + * + * <p>{@code DimensionalityReduction} specifies which dimensions to keep, and which grid + * values to use for the omitted dimensions. This information allows the conversion from + * a source {@link GridGeometry} to a reduced grid geometry, and conversely.</p> + * + * <p>Instances of {@code DimensionalityReduction} are immutable and thread-safe.</p> + * + * <h2>Assumptions</h2> + * The current implementation assumes that removing <var>n</var> dimensions in the grid extent + * causes the removal of exactly <var>n</var> dimensions in the Coordinate Reference System. + * However, the removed dimensions do not need to be at the same indices or in same order. + * + * @author Alexis Manin (Geomatys) + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ + public class DimensionalityReduction implements UnaryOperator<GridCoverage>, Serializable { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -6462887684250336261L; + + /** + * The source grid geometry with all dimensions. + * + * @see #getSourceGridGeometry() + */ + private final GridGeometry sourceGeometry; + + /** + * The reduced grid geometry. + * The number of dimensions shall be the number of bits set in the {@link #gridAxesToPass} bitmask. + * + * @see #getReducedGridGeometry() + */ + private final GridGeometry reducedGeometry; + + /** + * Indices of source grid dimensions to keep in the reduced grid. + * This is the parameter of the "pass-through" coordinate operation. + * Values must be in strictly increasing order. + * + * @see #getSelectedDimensions() + */ + private final int[] gridAxesToPass; + + /** + * Indices of target dimensions that have been removed. + * Values must be in strictly increasing order. + */ + private final int[] crsAxesToRemove; + + /** + * Partially filled array of CRS components to use for building a compound CRS. + * Elements in this array are either instances of {@link CoordinateReferenceSystem} or {@link Integer}. + * The CRS elements are components in the dimensions that were removed, while the integer elements are + * slots where to insert components of a reduced CRS with the number of dimensions given by the integer. + * This array is {@code null} if at least one CRS component cannot be isolated. + */ + @SuppressWarnings("serial") // Most SIS implementations are serializable. + private final Object[] componentsOfCRS; + + /** + * The part of the "grid to CRS" transform which has been removed in the reduced grid geometry. + * The number of source and target dimensions are the same than in the source grid geometry. + * The dimensions identified by {@link #gridAxesToPass} are pass-through dimensions. + * + * @see #getRemovedGridToCRS(PixelInCell) + */ + @SuppressWarnings("serial") // Most SIS implementations are serializable. + private final MathTransform removedGridToCRS, removedCornerToCRS; + + /** + * Grid coordinates to use in {@code reverse(…)} method calls for reconstituting some removed dimensions. + * Keys are grid dimensions of the source that are <em>not</em> in the {@link #gridAxesToPass} array. + * Values are grid coordinates to assign to the source grid extent at the dimension identified by the key. + * This map does not need to contain an entry for all removed dimensions. + * + * @see #getSliceCoordinates() + */ + @SuppressWarnings("serial") // Map.of(…) are serializable. + private final Map<Integer,Long> sliceCoordinates; + + /** + * A cache of {@link #gridAxesToPass} for all combinations of axes to retain in the first four dimensions. + * We use this cache because the same sequences of dimension indices will be created most of the times. + */ + private static final int[][] CACHED = new int[1 << 4][]; // Length must be a power of 2. + + /** + * Returns the indices for which {@code axes} contains a bit in the set state. + * This method may return a cached instance, <strong>do not modify.</strong> + * Elements in the returned array are in strictly increasing order. + */ + private static int[] toArray(final BitSet axes) { + if (axes.length() >= CACHED.length) { + return axes.stream().toArray(); + } + final int bitmask = (int) axes.toLongArray()[0]; + int[] indices; + synchronized (CACHED) { + indices = CACHED[bitmask]; + if (indices == null) { + CACHED[bitmask] = indices = axes.stream().toArray(); + } + } + return indices; + } + + /** + * Reduces the dimension of the specified grid geometry by retaining the axes specified in the given bitset. + * Axes in the reduced grid geometry will be in the same order than in the source geometry: + * + * @param source the grid geometry on which to select a subset of its grid dimensions. + * @param gridAxes bitmask of indices of source grid dimensions to keep in the reduced grid. + * Will be modified by this constructor for internal purpose. + * @param factory the factory to use for creating new math transforms, or {@code null} if none. + * @throws FactoryException if the dimensions to kept cannot be separated from the dimensions to omit. + */ + protected DimensionalityReduction(final GridGeometry source, final BitSet gridAxes, final MathTransformFactory factory) + throws FactoryException + { + gridAxesToPass = toArray(gridAxes); + sliceCoordinates = Map.of(); + sourceGeometry = source; + /* + * Set `gridAxes` to its complement: instead of dimensions to pass, it will become + * the dimensions to remove. If the result is empty, we have an identity operation. + */ + final int sourceDim = source.getDimension(); + gridAxes.flip(0, sourceDim); + if (gridAxes.isEmpty()) { + reducedGeometry = source; + crsAxesToRemove = ArraysExt.EMPTY_INT; + componentsOfCRS = null; + } else { + /* + * The calculation of `dimSubCRS` below assumes that 1 removed grid dimension + * implies 1 removed CRS dimension. See "assumptions" in class javadoc. + */ + final int targetDim = source.getTargetDimension(); + final int dimSubCRS = targetDim - (sourceDim - gridAxesToPass.length); + final var helper = new SliceGeometry(source, null, gridAxesToPass, factory); + reducedGeometry = helper.reduce(null, dimSubCRS); + /* + * Get the sequence of CRS axes to remove. The result will often be + * the same indices than `gridAxesToRemove`, but not necessarily. + */ + final BitSet crsAxes = bitmask(helper.getTargetDimensions(), targetDim); + crsAxes.flip(0, targetDim); + crsAxesToRemove = toArray(crsAxes); + if (source.isDefined(GridGeometry.CRS)) { + componentsOfCRS = filterCRS(source.getCoordinateReferenceSystem(), crsAxes); + } else { + componentsOfCRS = null; + } + if (source.isDefined(GridGeometry.GRID_TO_CRS)) { + final int[] gridAxesToRemove = gridAxes.stream().toArray(); + removedGridToCRS = filterGridToCRS(gridAxesToRemove, gridAxes, PixelInCell.CELL_CENTER, factory); + removedCornerToCRS = filterGridToCRS(gridAxesToRemove, gridAxes, PixelInCell.CELL_CORNER, factory); + return; + } + } + removedGridToCRS = null; + removedCornerToCRS = null; + } + + /** + * Returns all CRS components for the dimensions where the bit is set. + * There is one CRS for each range of consecutive dimension indices. + * If at least one CRS cannot be fetched, then this method returns {@code null}. + * + * @param crs the CRS for which to get components. + * @param axes dimensions (or axis indices) of the components to get. + * @throws FactoryException if the geodetic factory failed to create a compound CRS. + * @return CRS for each range of consecutive axis indices. + */ + private static Object[] filterCRS(final CoordinateReferenceSystem crs, final BitSet axes) + throws FactoryException + { + final int dim = crs.getCoordinateSystem().getDimension(); + final var components = new Object[dim]; + int count = 0; + int upper = 0; + int lower; + while ((lower = axes.nextSetBit(upper)) >= 0) { + if (lower != upper) { + components[count++] = lower - upper; // Here `upper` is not yet updated to the higher value. + } + upper = axes.nextClearBit(lower); + for (CoordinateReferenceSystem c : CRS.selectComponents(crs, ArraysExt.range(lower, upper))) { + components[count++] = c; + } + } + if (upper != dim) { + components[count++] = dim - upper; // Keep an empty slot for the reduced CRS component. + } + return ArraysExt.resize(components, count); + } + + /** + * Returns a "grid to CRS" transform which will transform only the "removed" dimensions. + * Other dimensions are passed-through. + * + * @param gridAxesToRemove the dimensions on which to operate. + * @param bitset same as {@link gridAxesToRemove} but as a bit set (for efficiency). + * @param anchor whether to compute the transform for pixel corner or pixel center. + * @param factory the factory to use for creating new math transforms, or {@code null} if none. + */ + private MathTransform filterGridToCRS(final int[] gridAxesToRemove, final BitSet bitset, final PixelInCell anchor, + final MathTransformFactory factory) throws FactoryException + { + final MathTransform gridToCRS = sourceGeometry.getGridToCRS(anchor); + final var sep = new TransformSeparator(gridToCRS, factory); + sep.addSourceDimensions(gridAxesToRemove); + sep.addTargetDimensions(crsAxesToRemove); + return PassThroughTransform.create(bitset, sep.separate(), gridToCRS.getSourceDimensions(), factory); + } + + /** + * Creates the same dimensionality reduction as the specified {@code source}, but with different slice indices. + * + * @param source the dimensionality reduction to copy. + * @param slice coordinates of the slice in removed dimensions. + */ + private DimensionalityReduction(final DimensionalityReduction source, final Map<Integer,Long> slice) { + sourceGeometry = source.sourceGeometry; + reducedGeometry = source.reducedGeometry; + gridAxesToPass = source.gridAxesToPass; + crsAxesToRemove = source.crsAxesToRemove; + componentsOfCRS = source.componentsOfCRS; + removedGridToCRS = source.removedGridToCRS; + removedCornerToCRS = source.removedCornerToCRS; + sliceCoordinates = Map.copyOf(slice); + } + + /** + * Returns a new bitmask of all dimension indices in the axes array. + * The returned object can be safely modified. + * + * @param axes indices of axes to pass or to remove. + * @param sourceDim maximal valid dimension index + 1. + * @return bitmask of dimensions in the given array. + * @throws IndexOutOfBoundsException if an axis index is out of bounds. + */ + private static BitSet bitmask(final int[] axes, final int sourceDim) { + final BitSet bitmask = new BitSet(sourceDim); + for (final int dim : axes) { + ArgumentChecks.ensureValidIndex(sourceDim, dim); + bitmask.set(dim); + } + return bitmask; + } + + /** + * Reduces the dimension of the specified grid geometry by retaining only the specified axes. + * Axes in the reduced grid geometry will be in the same order than in the source geometry: + * change of axis order and duplicated values in the {@code gridAxesToPass} argument are ignored. + * + * @param source the grid geometry to reduce. + * @param gridAxesToPass the grid axes to retain, ignoring order and duplicated values. + * @return reduced grid geometry together with other information. + * @throws IndexOutOfBoundsException if a grid axis index is out of bounds. + * @throws IllegalGridGeometryException if the dimensions to kept cannot be separated from the dimensions to omit. + */ + public static DimensionalityReduction select(final GridGeometry source, final int... gridAxesToPass) { + ArgumentChecks.ensureNonNull("source", source); + final BitSet bitmask = bitmask(gridAxesToPass, source.getDimension()); + try { + return new DimensionalityReduction(source, bitmask, null); + } catch (FactoryException e) { + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions, e)); + } + } + + /** + * A convenience method for selecting the two first dimensions of the specified grid geometry. + * This method can be used as a lambda function in resources query. Example: + * + * {@snippet lang="java" : + * CoverageQuery query = new CoverageQuery(); + * query.setAxisSelection(DimensionalityReduction::select2D); + * } + * + * @param source the grid geometry to reduce. + * @return reduced grid geometry together with other information. + * @throws IndexOutOfBoundsException if the grid geometry does not have at least two dimensions. + * @throws IllegalGridGeometryException if the dimensions to kept cannot be separated from the dimensions to omit. + * + * @see org.apache.sis.storage.CoverageQuery#setAxisSelection(Function) + */ + public static DimensionalityReduction select2D(final GridGeometry source) { + return select(source, 0, 1); + } + + /** + * Reduces the dimension of the specified grid geometry by removing the specified axes. + * Axes in the reduced grid geometry will be in the same order than in the source geometry: + * axis order and duplicated values in the {@code gridAxesToRemove} argument are not significant. + * + * @param source the grid geometry to reduce. + * @param gridAxesToRemove the grid axes to remove, ignoring order and duplicated values. + * @return reduced grid geometry together with other information. + * @throws IndexOutOfBoundsException if a grid axis index is out of bounds. + * @throws IllegalGridGeometryException if the dimensions to kept cannot be separated from the dimensions to omit. + */ + public static DimensionalityReduction remove(final GridGeometry source, final int... gridAxesToRemove) { + ArgumentChecks.ensureNonNull("source", source); + final int sourceDim = source.getDimension(); + final BitSet bitmask = bitmask(gridAxesToRemove, sourceDim); + bitmask.flip(0, sourceDim); + try { + return new DimensionalityReduction(source, bitmask, null); + } catch (FactoryException e) { + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions, e)); + } + } + + /** + * Automatically reduces a grid geometry by removing all grid dimensions with an extent size of 1. + * Axes in the reduced grid geometry will be in the same order than in the source geometry. + * + * @param source the grid geometry to reduce. + * @return reduced grid geometry together with other information. + * @throws IncompleteGridGeometryException if the grid geometry has no extent. + * @throws IllegalGridGeometryException if the dimensions to kept cannot be separated from the dimensions to omit. + * + * @see #select2D(GridGeometry) + */ + public static DimensionalityReduction reduce(final GridGeometry source) { + ArgumentChecks.ensureNonNull("source", source); + final GridExtent extent = source.getExtent(); + final int sourceDim = extent.getDimension(); + final BitSet bitmask = new BitSet(sourceDim); + for (int dim=0; dim < sourceDim; dim++) { + if (extent.getLow(dim) != extent.getHigh(dim)) { + bitmask.set(dim); + } + } + try { + return new DimensionalityReduction(source, bitmask, null); + } catch (FactoryException e) { + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions, e)); + } + } + + /** + * Returns {@code true} if this object does not reduce any dimension. + * It may happen if {@code select(…)} has been invoked with all axes to keep, + * or if {@code remove(…)} has been invoked with no axis to remove. + * + * @return whether this {@code DimensionalityReduction} does nothing. + */ + public boolean isIdentity() { + return reducedGeometry == sourceGeometry; + } + + /** + * Returns {@code true} if this dimensionality reduction is a slice in the source coverage. + * This is true if all removed dimensions either have a {@linkplain GridExtent#getSize(int) + * grid size} of one cell, or have a {@linkplain #getSliceCoordinates() slice coordinate} specified. + * + * <p>If this method returns {@code false}, then the results of {@code reverse(…)} method calls + * are potentially ambiguous and may cause a {@link SubspaceNotSpecifiedException} to be thrown + * at {@linkplain GridCoverage#render(GridExtent) rendering} time.</p> + * + * @return whether this dimensionality reduction is a slice in the source coverage. + * + * @see #getSliceCoordinates() + * @see #withSlicePoint(long[]) + * @see #withSliceByRatio(double) + */ + public boolean isSlice() { + return indexOfNonSlice() >= 0; + } + + /** + * Ensures that {@link #isSlice()} returns {@code true}. + * + * @throws SubspaceNotSpecifiedException if this dimensionality reduction is not a slice of the source coverage. + */ + final void ensureIsSlice() throws SubspaceNotSpecifiedException { + final int dim = indexOfNonSlice(); + if (dim >= 0) { + throw new SubspaceNotSpecifiedException(Resources.format(Resources.Keys.AmbiguousGridAxisOmission_1, dim)); + } + } + + /** + * If {@link #isSlice()} would returns {@code false}, returns the index of the problematic dimension. + * Otherwise returns -1. This is used for more detailed error message. + */ + private int indexOfNonSlice() { + int i = gridAxesToPass.length - 1; + final GridExtent extent = sourceGeometry.getExtent(); + for (int dim = extent.getDimension(); --dim >= 0;) { + if (i >= 0 && dim == gridAxesToPass[i]) { + i--; + } else if (!sliceCoordinates.containsKey(dim)) { + if (extent.getLow(dim) != extent.getHigh(dim)) { + return dim; + } + } + } + return -1; + } + + /** + * Returns {@code true} if the given grid geometry is likely to be already reduced. + * Current implementation checks only the number of dimensions. + * + * @param candidate the grid geometry to test. + * @return whether the given grid geometry is likely to be already reduced. + */ + public boolean isReduced(final GridGeometry candidate) { + int dim; + if (candidate.extent == null && candidate.gridToCRS == null) { + dim = reducedGeometry.getTargetDimension(); + } else { + dim = reducedGeometry.getDimension(); + } + return candidate.getDimension() == dim; + } + + /** + * Returns the grid geometry with only the retained grid axis dimension. + * The number of CRS dimensions should be reduced as well, + * but not necessarily in a one-to-one relationship. + * + * @return the grid geometry with retained grid dimensions. + */ + public GridGeometry getReducedGridGeometry() { + return reducedGeometry; + } + + /** + * Returns the grid geometry with all grid axis dimension. + * This is the {@code source} argument given to factory methods. + * + * @return the grid geometry with all grid dimensions. + */ + public GridGeometry getSourceGridGeometry() { + return sourceGeometry; + } + + /** + * Returns the part of the "grid to CRS" transform which has been removed in the reduced grid geometry. + * This is a pass-through transform (potentially, but not necessarily, implemented + * by {@link org.apache.sis.referencing.operation.transform.PassThroughTransform}). + * The number of source dimensions is the same than in the source grid geometry. + * The dimensions that are passed-through are the dimensions on which the reduced grid geometry operates. + * + * @param anchor the cell part to map (center or corner). + * @return removed part of the conversion from grid coordinates to "real world" coordinates. + */ + private MathTransform getRemovedGridToCRS(final PixelInCell anchor) { + if (PixelInCell.CELL_CENTER.equals(anchor)) { + return removedGridToCRS; + } else if (PixelInCell.CELL_CORNER.equals(anchor)) { + return removedCornerToCRS; + } else { + return PixelTranslation.translate(removedGridToCRS, PixelInCell.CELL_CENTER, anchor); + } + } + + /** + * Returns the indices of the source dimensions that are kept in the reduced grid geometry. + * + * @return indices of source grid dimensions that are retained in the reduced grid geometry. + */ + public int[] getSelectedDimensions() { + return gridAxesToPass.clone(); + } + + /** + * Returns the grid coordinates used in {@code reverse(…)} method calls for reconstituting some removed dimensions. + * Keys are indices of grid dimensions in the source that are <em>not</em> retained in the reduced grid geometry. + * Values are grid coordinates to assign to those dimensions when a {@code reverse(…)} method is executed. + * + * <p>This map does not need to contain an entry for all removed dimensions. + * If no slice point is specified for a given dimension, then the {@code reverse(…)} methods will use the + * full range of grid coordinates specified in the {@linkplain #getSourceGridGeometry() source geometry}. + * Often, those ranges have a {@linkplain GridExtent#getSize(int) size} of 1, + * in which case methods such as {@link GridCoverage#render(GridExtent)} will work anyway. + * If a removed source grid dimension had a size greater than 1 and no slice coordinates is specified; + * then the {@code reverse(…)} methods in this class will still work but an + * {@link SubspaceNotSpecifiedException} may be thrown later by other classes.</p> + * + * <p>This map is initially empty. Slice coordinates can be specified by calls + * to {@link #withSlicePoint(long[])} or {@link #withSliceByRatio(double)}.</p> + * + * @return source grid coordinates of the slice point used in {@code reverse(…)} method calls. + * + * @see #withSlicePoint(long[]) + * @see #withSliceByRatio(double) + * @see GridExtent#getSliceCoordinates() + * @see GridCoverage.Evaluator#setDefaultSlice(Map) + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") // Map is immutable. + public Map<Integer,Long> getSliceCoordinates() { + return sliceCoordinates; + } + + /** + * Returns the dimension in the reduced grid for the given dimension in the source grid. + * If the specified source grid dimension is not retained, then this method returns a negative number. + * + * @param dim source dimension index to map to reduced dimension index. + * @return reduced dimension index, or a negative value if not mapped. + */ + final int toReducedDimension(final int dim) { + return Arrays.binarySearch(gridAxesToPass, dim); + } + + /** + * Returns the dimension in the source grid for the given dimension in the reduced grid. + * + * @param dim reduced dimension index to map to source dimension index. + * @return source dimension index. + * @throws IndexOutOfBoundsException if the given dimension is invalid. + */ + final int toSourceDimension(final int dim) { + return gridAxesToPass[dim]; + } + + /** + * Returns the dimension in the source CRS for given counter of removed dimension. + * + * @param i 0 for the first removed CRS dimension, 1 fo the second removed CRS dimension, <i>etc.</i> + * @return dimension in the source CRS which has been removed, of -1 if <var>i</var> is above bounds. + */ + private int toRemovedDimension(final int i) { + return (i < crsAxesToRemove.length) ? crsAxesToRemove[i] : -1; + } + + /** + * Ensures that {@code source} has the same number of dimensions and the same axes than {@code expected}. + * Only axis names that are specified in both extents are compared. + * If the {@code source} to validate is null, it defaults to the {@code expected} extent. + * + * @param expected grid extent with expected axes, or {@code null} if none. + * @param source grid extent to validate, or {@code null} if unspecified. + * @return whether the two extents are equal. + * @throws IllegalArgumentException if the number of dimensions or at least one axis name does not match. + */ + private static boolean ensureSameAxes(final GridExtent expected, final GridExtent source) { + if (source == null) { + return true; + } + if (expected != null) { + expected.ensureSameAxes(source, "source"); + if (expected.equals(source, ComparisonMode.IGNORE_METADATA)) { + return true; + } + } + return false; + } + + /** + * Returns {@code true} if the {@code actual} CRS is equal, ignore metadata, to the one in {@code expected}. + * If any CRS is null, this method conservatively returns {@code true}. + * This is used for assertions only. + */ + private static boolean assertSameCRS(final GridGeometry expected, final CoordinateReferenceSystem actual) { + if (actual != null && expected.isDefined(GridGeometry.CRS)) { + final var crs = expected.getCoordinateReferenceSystem(); + if (crs != null) { + return Utilities.deepEquals(crs, actual, ComparisonMode.DEBUG); + } + } + return true; + } + + /** + * Returns a coordinate tuple on which dimensionality reduction has been applied. + * The coordinate reference system of the given {@code source} should be either + * null or equal (ignoring metadata) to the CRS of the source grid geometry. + * For performance reason, this is not verified unless assertions are enabled. + * + * @param source the source coordinate tuple, or {@code null}. + * @return the reduced coordinate tuple, or {@code null} if the given source was null. + */ + @ArgumentCheckByAssertion + public DirectPosition apply(final DirectPosition source) { + if (source != null) { + ArgumentChecks.ensureDimensionMatches("source", sourceGeometry.getTargetDimension(), source); + assert assertSameCRS(sourceGeometry, source.getCoordinateReferenceSystem()) : source; + if (!isIdentity()) { + final var reduced = new GeneralDirectPosition(reducedGeometry.getTargetDimension()); + /* + * Following code is more complicated than what it could be if we stored a + * `crsAxesToPass` array in this object. But it may not be worth to store + * such array only for this method. + */ + int dim = -1, remCounter = 0, removedAxis = crsAxesToRemove[0]; + for (int i=0; i < reduced.coordinates.length; i++) { + while (++dim == removedAxis) { + removedAxis = toRemovedDimension(++remCounter); + } + reduced.coordinates[i] = source.getOrdinate(dim); + } + return reduced; + } + } + return source; + } + + /** + * Returns a grid extent on which dimensionality reduction has been applied. + * If the given source is {@code null}, then this method returns {@code null}. + * Nulls are accepted because they are valid argument values in calls to + * {@link GridCoverage#render(GridExtent)}. + * + * @param source the grid extent to reduce, or {@code null}. + * @return the reduced grid extent. May be {@code source}, which may be null. + * @throws MismatchedDimensionException if the given source does not have the expected number of dimensions. + * @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension. + * + * @see GridExtent#selectDimensions(int...) + */ + public GridExtent apply(final GridExtent source) { + if (source == null) return null; + if (ensureSameAxes(sourceGeometry.extent, source)) { + return reducedGeometry.extent; + } + return isIdentity() ? source : source.selectDimensions(gridAxesToPass); + } + + /** + * Returns a grid geometry on which dimensionality reduction of the grid extent has been applied. + * It usually implies a reduction in the number of dimensions of the CRS as well, + * but not necessarily in same order. + * + * <p>If the given source is {@code null}, then this method returns {@code null}. + * Nulls are accepted because they are valid argument values in calls to + * {@link org.apache.sis.storage.GridCoverageResource#read(GridGeometry, int...)}.</p> + * + * @param source the grid geometry to reduce, or {@code null}. + * @return the reduced grid geometry. May be {@code source}, which may be null. + * @throws MismatchedDimensionException if the given source does not have the expected number of dimensions. + * @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension. + * + * @see GridGeometry#selectDimensions(int...) + */ + public GridGeometry apply(final GridGeometry source) { + if (source == null) return null; + if (ensureSameAxes(sourceGeometry.extent, source.extent)) { + return reducedGeometry; + } + return isIdentity() ? source : source.selectDimensions(gridAxesToPass); + } + + /** + * Returns a grid coverage on which dimensionality reduction of the domain has been applied. + * This is a reduction in the number of dimensions of the grid extent. It usually implies a + * reduction in the number of dimensions of the CRS as well, but not necessarily in same order. + * The sample dimensions (coverage range) are unmodified. + * + * <p>The returned coverage is a <em>view</em>: changes in the source coverage + * are reflected immediately in the reduced coverage, and conversely.</p> + * + * <h4>Reversibility</h4> + * If {@link #isSlice()} returns {@code false}, + * then the results of {@link #reverse(GridExtent)} are ambiguous + * and calls to {@link GridCoverage#render(GridExtent)} may cause + * an {@link SubspaceNotSpecifiedException} to be thrown. + * Unless the specified {@code source} grid coverage knows how to handle those cases. + * + * @param source the grid coverage to reduce. + * @return the reduced grid coverage, or {@code source} if this object {@linkplain #isIdentity() is identity}. + * @throws MismatchedDimensionException if the given source does not have the expected number of dimensions. + * @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension. + * + * @see GridCoverageProcessor#selectGridDimensions(GridCoverage, int...) + */ + @Override + public GridCoverage apply(final GridCoverage source) { + ArgumentChecks.ensureNonNull("source", source); + ensureSameAxes(sourceGeometry.extent, source.getGridGeometry().extent); + return isIdentity() ? source : new ReducedGridCoverage(source, this); + } + + /** + * Returns a grid extent on which dimensionality reduction has been reverted. + * For all dimensions that were removed, grid coordinates will be set to the + * {@linkplain #getSliceCoordinates() slice coordinates} if specified, + * or to the original source grid coordinates otherwise. + * In the latter case, the reconstituted grid coordinates will be a single value + * if {@link #isSlice()} returns {@code true} (in which case the returned extent + * is unambiguous), or may be a (potentially ambiguous) range of values otherwise. + * + * <h4>Handling of null grid geometry</h4> + * If the given extent is {@code null}, then this method returns an extent + * with {@linkplain #getSliceCoordinates() slice coordinates} if they are known. + * If no slice coordinate has been specified, then this method returns {@code null}. + * Nulls are accepted because they are valid argument values + * in calls to {@link GridCoverage#render(GridExtent)}. + * + * @param reduced the reduced grid extent to revert, or {@code null}. + * @return the source grid extent. May be {@code reduced}, which may be null. + * @throws IncompleteGridGeometryException if the source grid geometry has no extent. + * @throws MismatchedDimensionException if the given extent does not have the expected number of dimensions. + * @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension. + */ + public GridExtent reverse(final GridExtent reduced) { + if (ensureSameAxes(reducedGeometry.extent, reduced)) { // Argument validation. + if (sliceCoordinates.isEmpty()) { // Must be after argument validation. + return sourceGeometry.extent; + } + } + if (isIdentity()) { + return reduced; + } + final GridExtent source = sourceGeometry.getExtent(); + final long[] coordinates = source.getCoordinates(); + final int m = coordinates.length >>> 1; + sliceCoordinates.forEach((dim, slice) -> { + coordinates[dim ] = slice; + coordinates[dim+m] = slice; + }); + if (reduced != null) { + for (int i=0; i < gridAxesToPass.length; i++) { + final int dim = gridAxesToPass[i]; + coordinates[dim] = reduced.getLow (i); + coordinates[dim+m] = reduced.getHigh(i); + } + } + return new GridExtent(source, coordinates); + } + + /** + * Returns a grid geometry on which dimensionality reduction has been reverted. + * For all dimensions that were removed, grid coordinates will be set to the + * {@linkplain #getSliceCoordinates() slice coordinates} if specified, + * or to the original source grid coordinates otherwise. + * In the latter case, the reconstituted dimensions will map a single coordinate value + * if {@link #isSlice()} returns {@code true} (in which case the returned grid geometry + * is unambiguous), or may map a (potentially ambiguous) range of grid coordinate values otherwise. + * + * <h4>Handling of null grid geometry</h4> + * If the given geometry is {@code null}, then this method returns a grid geometry + * with the {@linkplain #getSliceCoordinates() slice coordinates} if they are known. + * If no slice coordinate has been specified, then this method returns {@code null}. + * Nulls are accepted because they are valid argument values in calls to + * {@link org.apache.sis.storage.GridCoverageResource#read(GridGeometry, int...)}. + * + * @param reduced the reduced grid geometry to revert, or {@code null}. + * @return the source grid geometry. May be {@code reduced}, which may be null. + * @throws IncompleteGridGeometryException if the source grid geometry has no extent. + * @throws MismatchedDimensionException if the given geometry does not have the expected number of dimensions. + * @throws IllegalArgumentException if axis types are specified but inconsistent in at least one dimension. + */ + public GridGeometry reverse(final GridGeometry reduced) { + final GridExtent extent = (reduced != null) ? reduced.extent : null; + if (ensureSameAxes(reducedGeometry.extent, extent)) { // Argument validation. + if (sliceCoordinates.isEmpty()) { // Must be after argument validation. + return sourceGeometry; + } + } + if (isIdentity()) { + return reduced; + } + /* + * Build a compound CRS on a "best effort" basis. This operation is costly + * if `fullCRS(…)` must be invoked, so we try to use the existing CRS first. + */ + CoordinateReferenceSystem crs = null; + if (reduced.isDefined(GridGeometry.CRS) && sourceGeometry.isDefined(GridGeometry.CRS)) { + final CoordinateReferenceSystem reducedCRS = reduced.getCoordinateReferenceSystem(); + if (Utilities.equalsIgnoreMetadata(reducedGeometry.getCoordinateReferenceSystem(), reducedCRS)) { + crs = sourceGeometry.getCoordinateReferenceSystem(); + } else { + FactoryException cause = null; + try { + crs = fullCRS(reducedCRS); + } catch (FactoryException e) { + cause = e; + } + if (crs == null) { + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions, cause)); + } + } + } + /* + * Build an envelope and a resolution array where values at each dimension are copied + * either from the source geometry or from the specified reduced geometry. + */ + final int targetDim = sourceGeometry.getTargetDimension(); + final double[] lowerCorner = new double[targetDim]; + final double[] upperCorner = new double[targetDim]; + final double[] resolution = new double[targetDim]; + long nonLinears = 0; + int reducedDim = 0; + int remCounter = 0; + int removedAxis = crsAxesToRemove[0]; + for (int i=0; i<targetDim; i++) { + final GridGeometry source; + final int dim; + if (i == removedAxis) { + dim = removedAxis; + source = sourceGeometry; + removedAxis = toRemovedDimension(++remCounter); // Dimension of the next axis to remove. + } else { + dim = reducedDim++; + source = reduced; + } + lowerCorner[i] = source.envelope.getLower(dim); + upperCorner[i] = source.envelope.getUpper(dim); + resolution [i] = (source.resolution != null) ? source.resolution[dim] : Double.NaN; + if ((source.nonLinears & Numerics.bitmask(dim)) != 0) { + nonLinears |= Numerics.bitmask(i); + } + } + return new GridGeometry(reverse(extent), + fullGridToCRS(reduced, PixelInCell.CELL_CENTER), + fullGridToCRS(reduced, PixelInCell.CELL_CORNER), + new ImmutableEnvelope(lowerCorner, upperCorner, crs), + ArraysExt.allEquals(resolution, Double.NaN) ? null : resolution, + nonLinears); + } + + /** + * Builds a CRS with the same number of axes than the CRS of the source geometry. + * Axes are copied either from the source CRS or the reduced CRS, depending on + * whether the corresponding dimension is present in the reduced CRS. + * + * @param reduced the CRS to inflate to the same number of dimensions than the source CRS. + * @return the "inflated" CRS. + */ + private CoordinateReferenceSystem fullCRS(final CoordinateReferenceSystem reduced) throws FactoryException { + if (componentsOfCRS == null) { + return null; + } + final var components = new CoordinateReferenceSystem[componentsOfCRS.length]; + int lower = 0; + for (int i=0; i<components.length; i++) { + final Object element = componentsOfCRS[i]; + if (element instanceof CoordinateReferenceSystem) { + components[i] = (CoordinateReferenceSystem) element; + } else { + final int upper = lower + (Integer) element; + components[i] = CRS.getComponentAt(reduced, lower, upper); + lower = upper; + } + } + return CRS.compound(components); + } + + /** + * Builds a transform with the same number of dimensions that the transform of the source geometry. + * + * @param reduced the transform to inflate to the same number of dimensions that the source geometry. + * @param anchor whether the transform map pixel centers or pixel corners. + * @return the "inflated" transform. + */ + private MathTransform fullGridToCRS(final GridGeometry reduced, final PixelInCell anchor) { + final MathTransform removed = getRemovedGridToCRS(anchor); + if (removed == null || !reduced.isDefined(GridGeometry.GRID_TO_CRS)) { + return null; + } + MathTransform gridToCRS = reduced.getGridToCRS(anchor); + if (Utilities.equalsIgnoreMetadata(reducedGeometry.getGridToCRS(anchor), gridToCRS)) { + return sourceGeometry.getGridToCRS(anchor); + } + gridToCRS = MathTransforms.passThrough(gridAxesToPass, gridToCRS, removed.getTargetDimensions()); + return MathTransforms.concatenate(removed, gridToCRS); + } + + /** + * Returns a dimensional reduction which will use the given source grid indices for {@code reverse(…)} operations. + * The length of the given {@code slicePoint} array shall be the number of dimensions of the source grid geometry. + * All given coordinate values shall be inside the source grid extent. + * + * @param point grid coordinates of a point located on the slice. + * @return the dimensionality reduction with the given slice point used for reverse operations. + * @throws IncompleteGridGeometryException if the source grid geometry has no extent. + * @throws MismatchedDimensionException if the given point does not have the expected number of dimensions. + * @throws PointOutsideCoverageException if the given point is outside the source grid extent. + */ + public DimensionalityReduction withSlicePoint(final long[] point) { + ArgumentChecks.ensureNonNull("point", point); + final GridExtent extent = sourceGeometry.getExtent(); + final int sourceDim = extent.getDimension(); - ArgumentChecks.ensureDimensionMatches("slicePoint", sourceDim, extent); + final Map<Integer,Long> slices = new HashMap<>(); + for (int dim=0; dim < sourceDim; dim++) { + final long low = extent.getLow (dim); + final long high = extent.getHigh(dim); + final long value = point[dim]; + if (value < low || value > high) { + String b = Arrays.toString(point); + b = b.substring(1, b.length() - 1); // Remove brackets. + throw new PointOutsideCoverageException(Resources.format( + Resources.Keys.GridCoordinateOutsideCoverage_4, + extent.getAxisIdentification(dim,dim), low, high, b)); + } + if (low != high && toReducedDimension(dim) < 0) { + slices.put(dim, value); + } + } + return slices.equals(sliceCoordinates) ? this : new DimensionalityReduction(this, slices); + } + + /** + * Returns a dimensional reduction with a relative slice position + * for every grid dimensions that have been removed. + * The relative position is specified by a ratio between 0 and 1 where + * 0 maps to {@linkplain GridExtent#getLow(int) low} grid coordinates, + * 1 maps to {@linkplain GridExtent#getHigh(int) high grid coordinates} and + * 0.5 maps to the median position. + * + * @param ratio the ratio to apply on all removed grid dimensions. + * @return the dimensionality reduction with the given slice ratio applied. + * @throws IncompleteGridGeometryException if the source grid geometry has no extent. + * @throws IllegalArgumentException if the given ratio is not between 0 and 1 inclusive. + * + * @see GridExtent#getRelative(int, double) + * @see GridDerivation#sliceByRatio(double, int...) + */ + public DimensionalityReduction withSliceByRatio(final double ratio) { + ArgumentChecks.ensureBetween("ratio", 0, 1, ratio); + final GridExtent extent = sourceGeometry.getExtent(); + final int sourceDim = extent.getDimension(); + final Map<Integer,Long> slices = new HashMap<>(); + for (int dim=0; dim < sourceDim; dim++) { + if (toReducedDimension(dim) < 0 && extent.getLow(dim) != extent.getHigh(dim)) { + slices.put(dim, extent.getRelative(dim, ratio)); + } + } + return slices.equals(sliceCoordinates) ? this : new DimensionalityReduction(this, slices); + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java index cf89e64a46,307800bd33..d5974b88fd --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java @@@ -24,10 -26,6 +24,10 @@@ import org.apache.sis.util.ArgumentChec * A view over the low or high grid envelope coordinates. * This is not a general-purpose grid coordinates since it assumes a {@link GridExtent} coordinates layout. * - * <div class="note"><b>Upcoming API generalization:</b> ++ * <h2>Upcoming API generalization</h2> + * this class may implement the {@code GridCoordinates} interface in a future Apache SIS version. - * This is pending GeoAPI update.</div> ++ * This is pending GeoAPI update. + * * @author Martin Desruisseaux (Geomatys) * @version 1.1 * @since 1.0 diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java index 6690fca29a,2aeb42d671..6805ddfadf --- a/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java @@@ -22,11 -22,11 +22,10 @@@ import java.util.LinkedHashSet import org.apache.sis.util.ArgumentChecks; import org.apache.sis.internal.util.CollectionsExt; import org.apache.sis.internal.util.UnmodifiableArrayList; - import org.apache.sis.util.resources.Errors; // Branch-dependent imports -import org.opengis.filter.Filter; -import org.opengis.filter.LogicalOperator; -import org.opengis.filter.LogicalOperatorName; +import org.apache.sis.internal.geoapi.filter.LogicalOperator; +import org.apache.sis.internal.geoapi.filter.LogicalOperatorName; /** diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CommonDomainFinder.java index 0000000000,4fa3b4b157..8ff67d02f1 mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CommonDomainFinder.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CommonDomainFinder.java @@@ -1,0 -1,376 +1,385 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.sis.internal.coverage; + + import java.util.Map; + import java.util.LinkedHashMap; + import java.util.NoSuchElementException; + import org.opengis.util.FactoryException; + import org.opengis.geometry.MismatchedDimensionException; + import org.opengis.referencing.datum.PixelInCell; + import org.opengis.referencing.operation.Matrix; + import org.opengis.referencing.operation.MathTransform; + import org.opengis.referencing.operation.TransformException; + import org.opengis.referencing.crs.CoordinateReferenceSystem; + import org.opengis.referencing.operation.NoninvertibleTransformException; + import org.apache.sis.referencing.CRS; + import org.apache.sis.referencing.operation.transform.MathTransforms; + import org.apache.sis.coverage.grid.GridExtent; + import org.apache.sis.coverage.grid.GridGeometry; + import org.apache.sis.coverage.grid.IllegalGridGeometryException; + import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix; + import org.apache.sis.internal.feature.Resources; + import org.apache.sis.internal.util.Numerics; + import org.apache.sis.util.Numbers; + + + /** + * Helper class for building a combined domain from a list of grid geometries. + * After construction, one of the following methods shall be invoked exactly once. + * + * <ul> + * <li>{@link #setFromGridAligned(GridGeometry...)}</li> + * </ul> + * + * Then, the result can be obtained by the given methods: + * + * <ul> + * <li>{@link #result()}</li> + * <li>{@link #gridTranslations()}</li> + * <li>{@link #sourceOfGridToCRS()}</li> + * </ul> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ + public final class CommonDomainFinder { + /** + * Whether to compute intersection (true) or union (false) of all grid coverages. + * This is not yet configurable in current version. + * + * <p>We use this constant for tracking the code to update when we will want to provide an option for using + * union or strict equality instead (the latter would be a mode that fails if all extents are not identical). + * Note that in the case of unions, it would be possible to specify coverages with no intersection. + * Whether we should accept that or raise an exception is still an open question.</p> + */ + public static final boolean INTERSECTION = true; + + /** + * The grid geometry taken as the reference for computing the translations of all grid geometry items. + * Values of {@link #gridTranslations} and {@link #itemTranslations} are relative to that reference. + * At first this is an arbitrary grid geometry, for example the first encountered one. + * After {@code CommonDomainFinder} completed its task, this is updated to the final result. + * + * @see #result() + */ + private GridGeometry reference; + + /** + * Coordinate reference system of the reference, or {@code null} if not yet known. + * It will be set to the CRS of the first grid geometry where the CRS is defined. + */ + private CoordinateReferenceSystem crs; + + /** + * The inverse of the "grid to CRS" transform of the grid geometry taken as a reference. + */ + private MathTransform crsToGrid; + + /** + * The convention to use for fetching the "grid to CRS" transforms. + */ + private final PixelInCell anchor; + + /** + * The combined extent, as the union or intersection of all grid extents. + */ + private GridExtent extent; + + /** + * The translation in units of grid cells from the {@linkplain #reference} grid geometry + * to the grid geometry in the key. + */ + private final Map<GridGeometry,long[]> gridTranslations; + + /** + * Translations in units of grid cells from the {@linkplain #reference} grid geometry to each item. + * For each index <var>i</var>, {@code itemTranslations[i]} is a value from {@link #gridTranslations} + * map and may be reused at more than one index <var>i</var>. + * + * @see #gridTranslations() + */ + private long[][] itemTranslations; + + /** + * If one of the grid geometries has the same "grid to CRS" than the common grid geometry, the index. + * Otherwise -1. + */ + private int sourceOfGridToCRS; + + /** + * Creates a new common domain finder. + * + * @param anchor the convention to use for fetching the "grid to CRS" transforms. + */ + CommonDomainFinder(final PixelInCell anchor) { + this.anchor = anchor; + gridTranslations = new LinkedHashMap<>(); + } + + /** + * Computes a common grid geometry from the given items. + * All items shall share be aligned on the same grid. + * Items may be translated relative to each other, + * but the translations shall be an integer number of grid cells. + * + * <h4>Coordinate reference system</h4> + * If the CRS of a grid geometry is undefined, it is assumed the same than other grid geometries. + * + * @param items the grid geometries for which to compute a common grid geometry. + * @throws IllegalGridGeometryException if the specified item is not compatible with the reference grid geometry. + */ + final void setFromGridAligned(final GridGeometry... items) { + itemTranslations = new long[items.length][]; + for (int i=0; i<items.length; i++) { + itemTranslations[i] = gridTranslations.computeIfAbsent(items[i], this::itemToCommon); + } + /* + * Change the reference grid geometry for matching more closely the desired grid extent. + * If one item has exactly the desired grid extent, use it. Otherwise search for an item + * having the same origin. This criterion is arbitrary and may change in future version. + */ + GridGeometry fallback = null; + GridExtent location = null; + for (final Map.Entry<GridGeometry,long[]> entry : gridTranslations.entrySet()) { + final GridGeometry item = entry.getKey(); + final GridExtent actual = item.getExtent(); + final GridExtent expected = extent.translate(entry.getValue()); + if (actual.equals(expected)) { + setGridToCRS(items, item); // Must be before `reference` assignation. + reference = item; + return; + } + // Arbitrary criterion (may be revisited in any future version). - if (fallback == null && expected.getLow().equals(actual.getLow())) { ++ if (fallback == null && sameLow(expected, actual)) { + location = expected; + fallback = item; + } + } + if (fallback == null) { + fallback = reference; + location = extent; + } + setGridToCRS(items, fallback); + try { + reference = fallback.relocate(location); + } catch (TransformException e) { + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries), e); + } + } + ++ private static boolean sameLow(final GridExtent e1, final GridExtent e2) { ++ final int dim = e1.getDimension(); ++ if (dim != e2.getDimension()) return false; ++ for (int i=0; i<dim; i++) { ++ if (e1.getLow(i) != e2.getLow(i)) return false; ++ } ++ return true; ++ } ++ + /** + * Given a grid geometry with the desired "grid to CRS", saves its index in {@link #sourceOfGridToCRS}. + * This method updates all previously computed translations for making them relative to the new reference. + * + * Note: updating values of the {@link #gridTranslations} map indirectly update all + * {@link #itemTranslations} array elements. + */ + private void setGridToCRS(final GridGeometry[] items, final GridGeometry item) { + sourceOfGridToCRS = indexOf(items, item); + if (item == reference) { // Quick check for a common case. + return; + } + final long[] oldReference = itemTranslations[indexOf(items, reference)]; + final long[] newReference = itemTranslations[sourceOfGridToCRS]; + final long[] change = new long[newReference.length]; + for (int i=0; i < newReference.length; i++) { + change[i] = Math.subtractExact(newReference[i], oldReference[i]); + } + for (final long[] offset : gridTranslations.values()) { + for (int i=0; i < offset.length; i++) { + offset[i] = Math.subtractExact(offset[i], change[i]); + } + } + } + + /** + * Returns the index of the given grid geometry. + * This method is invoked when the instance should always exist in the array. + */ + private static int indexOf(final GridGeometry[] items, final GridGeometry item) { + for (int i=0; i<items.length; i++) { + if (items[i] == item) { + return i; + } + } + throw new NoSuchElementException(); // Should never happen. + } + + /** + * Returns the common grid geometry computed from all specified items. + * + * @return a grid geometry which is the union or intersection of all specified items. + */ + final GridGeometry result() { + if (crs != null && !reference.isDefined(GridGeometry.CRS)) { + reference = new GridGeometry(extent, anchor, reference.getGridToCRS(anchor), crs); + } + return reference; + } + + /** + * Returns the translations (in units of grid cells) from the common grid geometry to all items. + * The items are the arguments given to {@link #setFromGridAligned(GridGeometry...)}, in order. + * The common grid geometry is the value returned by {@link #result()}. + * + * <p>The returned array should not be modified because it is not cloned.</p> + * + * @return translation from the common grid geometry to all items. This array is not cloned. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + final long[][] gridTranslations() { + return itemTranslations; + } + + /** + * If one items has the same "grid to CRS" than the common grid geometry, returns its index. + * + * @return index of an items having the desired "grid to CRS", or -1 if none. + */ + final int sourceOfGridToCRS() { + return sourceOfGridToCRS; + } + + /** + * Computes the translation in units of grid cells from the common grid geometry to the given item. + * This method also opportunistically computes the union or intersection of all grid extents. + * + * @param item the grid geometry for which to get a translation from the common grid geometry. + * @return translation in unis of grid cells. Note that the caller may reuse this array for many grid geometries. + * @throws IllegalGridGeometryException if the specified item is not compatible with the reference grid geometry. + */ + private long[] itemToCommon(final GridGeometry item) { + /* + * Compute the change ourselves instead of invoking `GridGeometry.createTransformTo(…)` + * because we do not want wraparound handling when we search for a simple translation. + */ + MathTransform change = item.getGridToCRS(anchor); + try { + if (crsToGrid == null) { + crsToGrid = change.inverse(); + reference = item; + } + if (item.isDefined(GridGeometry.CRS)) { + final CoordinateReferenceSystem src = item.getCoordinateReferenceSystem(); + if (crs == null) { + crs = src; + } else { + /* + * Ask for a change of CRS without specifying an area of interest (AOI) on the assumption + * that if the transformation is only a translation, the AOI would not make a difference. + * It save not only the AOI computation cost, but also make easier for `findOperation(…)` + * to use its cache. + */ + change = MathTransforms.concatenate(change, CRS.findOperation(src, crs, null).getMathTransform()); + } + } + change = MathTransforms.concatenate(change, crsToGrid); + } catch (FactoryException | NoninvertibleTransformException | MismatchedDimensionException e) { + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries), e); + } + final long[] offset = integerTranslation(MathTransforms.getMatrix(change), null); + if (offset == null) { + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries)); + } + /* + * The grid geometry has been accepted as valid. Now compute the combined extent, + * taking offset in account. At this point this is an offset TO the common grid geometry. + * It will be converted to an offset FROM the common grid geometry after the extent update. + */ + final GridExtent e = item.getExtent().translate(offset); + if (extent == null) { + extent = e; + } else if (INTERSECTION) { + extent = extent.intersect(e); + } else if (!extent.equals(e)) { + throw new IllegalGridGeometryException(); + } + for (int i=0; i<offset.length; i++) { + offset[i] = Math.negateExact(offset[i]); + } + return offset; + } + + /** + * If the given matrix is the identity matrix except for translation terms, returns the translation. + * Translation terms must be integer values and will be stored in the given {@code offset} array. + * If the matrix is not an integer translation, this method return {@code null} without modifying + * the given {@code offset} array. + * + * @param change conversion between two grid geometries, or {@code null}. + * @param offset where to store translation terms if the change is an integer translation, or {@code null}. + * @return the translation terms, or {@code null} if the given matrix does not met the conditions. + * + * @see org.apache.sis.referencing.operation.matrix.Matrices#isTranslation(Matrix) + */ + public static long[] integerTranslation(final Matrix change, long[] offset) { + if (change == null) { + return null; + } + final int numRows = change.getNumRow(); + final int numCols = change.getNumCol(); + for (int j=0; j<numRows; j++) { + for (int i=0; i<numCols; i++) { + double tolerance = Numerics.COMPARISON_THRESHOLD; + double e = change.getElement(j, i); + if (i == j) { + e--; + } else if (i == numCols - 1) { + final double a = Math.abs(e); + if (a > 1) { + tolerance = Math.min(tolerance*a, 0.125); + } + e -= Math.rint(e); + } + if (!(Math.abs(e) <= tolerance)) { // Use `!` for catching NaN. + return null; + } + } + } + /* + * Store the translation terms after we have determined that they are integers. + * It must be an "all or nothing" operation (unless an exception is thrown). + */ + if (offset == null) { + offset = new long[numRows - 1]; + } + final int i = numCols - 1; + if (change instanceof ExtendedPrecisionMatrix) { + final var epm = (ExtendedPrecisionMatrix) change; + for (int j=0; j<offset.length; j++) { + final Number e = epm.getElementOrNull(j, i); + offset[j] = (e != null) ? Numbers.round(e) : 0; + } + } else { + for (int j=0; j<offset.length; j++) { + offset[j] = Math.round(change.getElement(j, i)); + } + } + return offset; + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java index 752414f502,9d46610846..f153e9e9dd --- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java @@@ -41,9 -43,9 +42,9 @@@ import org.apache.sis.filter.Expression * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.4 * - * @param <R> the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs. + * @param <R> the type of resources (e.g. {@code Feature}) used as inputs. * * @since 1.1 */ diff --cc core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java index 0000000000,565bc0af2d..ef4f109054 mode 000000,100644..100644 --- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/DimensionalityReductionTest.java @@@ -1,0 -1,194 +1,194 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.sis.coverage.grid; + + import org.opengis.referencing.crs.SingleCRS; + import org.opengis.referencing.crs.CoordinateReferenceSystem; + import org.opengis.referencing.datum.PixelInCell; + import org.opengis.referencing.operation.Matrix; + import org.apache.sis.internal.referencing.DirectPositionView; + import org.apache.sis.referencing.operation.matrix.Matrix3; + import org.apache.sis.referencing.operation.matrix.Matrix4; + import org.apache.sis.referencing.operation.matrix.Matrices; + import org.apache.sis.referencing.operation.transform.MathTransforms; + import org.apache.sis.referencing.crs.HardCodedCRS; + import org.apache.sis.referencing.CRS; + import org.apache.sis.util.ComparisonMode; + import org.apache.sis.util.Utilities; + import org.apache.sis.test.TestCase; + import org.junit.Test; + -import static org.opengis.test.Assert.*; ++import static org.apache.sis.test.Assert.*; + + + /** + * Tests {@link DimensionalityReduction}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ + public final class DimensionalityReductionTest extends TestCase { + /** + * Convenience method for building a grid geometry. + * + * @param low low grid coordinates, inclusive. + * @param high high grid coordinates, inclusive. + * @param gridToCRS map pixel corner to geographic coordinates. + * @param crs target of {@code gridToCRS}. + * @return grid geometry for testing purposes. + */ + private static GridGeometry createGridGeometry(long[] low, long[] high, Matrix gridToCRS, CoordinateReferenceSystem crs) { + return new GridGeometry(new GridExtent(null, low, high, true), PixelInCell.CELL_CORNER, + MathTransforms.linear(gridToCRS), crs); + } + + /** + * Creates a four-dimensional grid geometry to be used for the tests in this class. + * The "grid to CRS" transform is a linear one. + */ + private static GridGeometry linearGrid() { + return createGridGeometry(new long[] { 20, 136, 4, -2}, + new long[] {419, 201, 10, 2}, + gridToCRS(), HardCodedCRS.GEOID_4D); + } + + /** + * Returns a "grid to CRS" transform for the full grid geometry to be used in tests. + */ + private static Matrix gridToCRS() { + return Matrices.create(5, 5, new double[] { + 0.5, 0, 0, 0, -180, + 0, 0.5, 0, 0, -90, + 0, 0, 5, 0, -2, + 0, 0, 0, 7, 60000, + 0, 0, 0, 0, 1}); + } + + /** + * Returns the matrix for the "grid to CRS" transform of the test grid, but without height. + */ + private static Matrix4 withHeightRemoved() { + return new Matrix4(0.5, 0, 0, -180, + 0, 0.5, 0, -90, + 0, 0, 7, 60000, + 0, 0, 0, 1); + } + + /** + * Returns the matrix for the "grid to CRS" transform with only the horizontal part. + */ + private static Matrix3 withHorizontal() { + return new Matrix3(0.5, 0, -180, + 0, 0.5, -90, + 0, 0, 1); + } + + /** + * Asserts that the "grid to CRS" transform of the given grid geometry is equal to the specified value. + * + * @param test the grid geometry to verify. + * @param expected the expected "grid to CRS" transform. + * @return the CRS of the given grid geometry. + */ + private static CoordinateReferenceSystem verifyGridToCRS(final GridGeometry test, final Matrix expected) { + Matrix actual = MathTransforms.getMatrix(test.getGridToCRS(PixelInCell.CELL_CORNER)); + assertMatrixEquals("gridToCRS", expected, actual, STRICT); + return test.getCoordinateReferenceSystem(); + } + + /** + * Tests reduction of a direct position. + * + * @param reduction the reduction to apply. + * @param source source coordinates. + * @param target expected reduced coordinates. + */ + private static void testPosition(final DimensionalityReduction reduction, double[] source, double[] target) { + assertArrayEquals(target, reduction.apply(new DirectPositionView.Double(source)).getCoordinate(), STRICT); + } + + /** + * Tests the removal of a single dimension in the middle of the "grid to CRS" transform. + * This test use the same CRS for all steps. + */ + @Test + public void testRemoval() { + var reduction = DimensionalityReduction.remove(linearGrid(), 2); // Remove height. + var crs = verifyGridToCRS(reduction.getReducedGridGeometry(), withHeightRemoved()); + assertArrayEquals(new SingleCRS[] {HardCodedCRS.WGS84, HardCodedCRS.TIME}, + CRS.getSingleComponents(crs).toArray()); + /* + * Tests the fast path in reduction and reverse operations. + */ + assertSame(reduction.getReducedGridGeometry(), reduction.apply(linearGrid())); + assertSame(reduction.getSourceGridGeometry(), reduction.reverse(reduction.getReducedGridGeometry())); + /* + * Test the reverse operation with a slightly different "grid to CRS". + * It will test the generic path, as opposed to above-cited fast path. + */ + GridGeometry test = reduction.reverse(createGridGeometry( + new long[] {100, 180, -5}, + new long[] {200, 195, -3}, + new Matrix4(0.5, 0, 0, -100, + 0, 0.5, 0, -80, + 0, 0, 7, 60002, + 0, 0, 0, 1), crs)); + + crs = verifyGridToCRS(test, Matrices.create(5, 5, new double[] { + 0.5, 0, 0, 0, -100, + 0, 0.5, 0, 0, -80, + 0, 0, 5, 0, -2, + 0, 0, 0, 7, 60002, + 0, 0, 0, 0, 1})); + + assertSame(reduction.getSourceGridGeometry().getCoordinateReferenceSystem(), crs); + testPosition(reduction, new double[] {100, 101, 102, 103}, new double[] {100, 101, 103}); + } + + /** + * Tests the selection of two dimensions. + */ + @Test + public void testSelect() { + var reduction = DimensionalityReduction.select2D(linearGrid()); + var gridToCRS = reduction.getReducedGridGeometry().getGridToCRS(PixelInCell.CELL_CORNER); + assertMatrixEquals("gridToCRS", withHorizontal(), MathTransforms.getMatrix(gridToCRS), STRICT); + assertSame(HardCodedCRS.WGS84, reduction.getReducedGridGeometry().getCoordinateReferenceSystem()); + + GridGeometry test = reduction.reverse(createGridGeometry( + new long[] {380, 100}, + new long[] {400, 200}, + new Matrix3(0, 0.5, -80, + 0.5, 0, -100, + 0, 0, 1), HardCodedCRS.WGS84_LATITUDE_FIRST)); + + var crs = verifyGridToCRS(test, Matrices.create(5, 5, new double[] { + 0, 0.5, 0, 0, -80, + 0.5, 0, 0, 0, -100, + 0, 0, 5, 0, -2, + 0, 0, 0, 7, 60000, + 0, 0, 0, 0, 1})); + /* + * CRS should have different axis order. + */ + var sourceCRS = reduction.getSourceGridGeometry().getCoordinateReferenceSystem(); + assertFalse(Utilities.equalsIgnoreMetadata(sourceCRS, crs)); + assertTrue(Utilities.deepEquals(test, test, ComparisonMode.ALLOW_VARIANT)); // Ignore axis order. + testPosition(reduction, new double[] {100, 101, 102, 103}, new double[] {100, 101}); + } + } diff --cc core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java index a716370dbf,00185fec10..0b692d6e3f --- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java @@@ -285,6 -303,50 +290,50 @@@ public final class PassThroughTransform 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, - 0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), null); + 0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), 0); } + + /** + * Tests the creation of a pass-through transform with modified coordinates that are not consecutive. + * This is a test of {@link PassThroughTransform#create(BitSet, MathTransform, int, MathTransformFactory)} + * factory method. + * + * @throws FactoryException if an error occurred while combining the transforms. + * @throws TransformException if a test coordinate tuple cannot be transformed. + */ + @Test + public void testNonConsecutiveModifiedCoordinates() throws FactoryException, TransformException { + random = TestUtilities.createRandomNumberGenerator(); + /* + * First, create a pass-through transform from an inseparable `PseudoTransform`. + * Because `PseudoTransform` is inseparable, the modified coordinates must be consecutive. + * However the `PassThroughTransform` result is partially separable and used in next step. + */ + final var bitset = new BitSet(); + bitset.set(1, 3, true); // Modified coordinates = {1, 2}. + MathTransform subTransform = new PseudoTransform(2, 2); + transform = PassThroughTransform.create(bitset, subTransform, 5, null); + assertEquals(5, transform.getSourceDimensions()); + assertEquals(5, transform.getTargetDimensions()); + assertEquals(1, ((PassThroughTransform) transform).firstAffectedCoordinate); + assertEquals(2, ((PassThroughTransform) transform).numTrailingCoordinates); + isInverseTransformSupported = false; + verifyTransform(subTransform, 1); + /* + * Now test with non-consecutive coordinates, except for the `PseudoTransform` part + * which must still be in consecutive coordinates. We add a linear transform before + * for making the work a little bit harder. + */ + bitset.clear(); + bitset.set(1, true); + bitset.set(3, 5, true); // The inseparable `PseudoTransform` part. + bitset.set(6, true); + bitset.set(9, true); + subTransform = MathTransforms.concatenate(MathTransforms.scale(4, 3, 7, 5, -6), transform); + transform = PassThroughTransform.create(bitset, subTransform, 10, null); + verifyTransform( + // _____________[0]_________[1]-____[2]____[3]_______[4] Dimension index in sub-transform. + new double[] {2, 1, -1, 0.2, 0.1, 9, 2, 8, 4, -1}, + new double[] {2, 4, -1, 1600.6, 2700.7, 9, 10, 8, 4, 6}); + } } diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java index b100426b1c,4c3a19aae4..10f05d2c7e --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java @@@ -51,17 -51,15 +51,15 @@@ public class MemoryFeatureSet extends A /** * Creates a new set of features stored in memory. It is caller responsibility to ensure that - * <code>{@linkplain Feature#getType()} == type</code> for all elements in the given collection + * <code>{@linkplain AbstractFeature#getType()} == type</code> for all elements in the given collection * (this is not verified). * - * @param parent listeners of the parent resource, or {@code null} if none. + * @param parent the parent resource, or {@code null} if none. * @param type the type of all features in the given collection. * @param features collection of stored features. This collection will not be copied. */ - public MemoryFeatureSet(final StoreListeners parent, - final DefaultFeatureType type, final Collection<AbstractFeature> features) - { - super(parent, false); - public MemoryFeatureSet(final Resource parent, final FeatureType type, final Collection<Feature> features) { ++ public MemoryFeatureSet(final Resource parent, final DefaultFeatureType type, final Collection<AbstractFeature> features) { + super(parent); ArgumentChecks.ensureNonNull("type", type); ArgumentChecks.ensureNonNull("features", features); this.type = type; diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java index b8af67c437,b065ce9b73..a6e50e038f --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java @@@ -33,7 -34,7 +33,8 @@@ import org.apache.sis.coverage.grid.Gri import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridRoundingMode; +import org.apache.sis.coverage.CannotEvaluateException; + import org.apache.sis.internal.coverage.RangeArgument; import org.apache.sis.storage.AbstractGridCoverageResource; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.RasterLoadingStrategy; diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java index da537fb5a9,cc9f56d6e3..39f5f760d5 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java @@@ -32,12 -32,12 +32,12 @@@ import org.apache.sis.internal.util.Col import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.internal.storage.Resources; import org.apache.sis.storage.AbstractFeatureSet; - import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.storage.Query; + import org.apache.sis.storage.Resource; // Branch-dependent imports -import org.opengis.feature.Feature; -import org.opengis.feature.FeatureType; +import org.apache.sis.feature.AbstractFeature; +import org.apache.sis.feature.DefaultFeatureType; /** diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java index ef38d87834,898e02089e..61931e80bf --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java @@@ -200,13 -202,13 +200,13 @@@ public class JoinFeatureSet extends Agg * @param rightAlias name of the associations to the {@code right} features, or {@code null} for a default name. * @param joinType whether values on both sides are required (inner join), or only one side (outer join). * @param condition join condition as <var>property from left feature</var> = <var>property from right feature</var>. - * @param featureInfo information about the {@link FeatureType} of this feature set. + * @param featureInfo information about the {@code FeatureType} of this feature set. * @throws DataStoreException if an error occurred while creating the feature set. */ - public JoinFeatureSet(final StoreListeners parent, + public JoinFeatureSet(final Resource parent, final FeatureSet left, String leftAlias, final FeatureSet right, String rightAlias, - final Type joinType, final BinaryComparisonOperator<? super Feature> condition, + final Type joinType, final BinaryComparisonOperator<? super AbstractFeature> condition, Map<String,?> featureInfo) throws DataStoreException {