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 9c514b4f87b529794374580972be724c4e4d7c55 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Mar 26 15:42:08 2022 +0100 Move `RangeInternal` to a separated class. The intent is to keep it internal while moving `AbstractGridResource` to a public package. --- .../apache/sis/internal/netcdf/RasterResource.java | 3 +- .../sis/internal/storage/AbstractGridResource.java | 368 +------------------- .../sis/internal/storage/MemoryGridResource.java | 2 +- .../apache/sis/internal/storage/RangeArgument.java | 386 +++++++++++++++++++++ .../sis/internal/storage/TiledGridResource.java | 2 +- .../internal/storage/MemoryGridResourceTest.java | 2 +- ...ridResourceTest.java => RangeArgumentTest.java} | 20 +- .../apache/sis/test/suite/StorageTestSuite.java | 2 +- 8 files changed, 414 insertions(+), 371 deletions(-) diff --git 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 index 499f55e..9ba27aa 100644 --- 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 @@ -55,6 +55,7 @@ import org.apache.sis.util.resources.Errors; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.internal.jdk9.JDK9; import org.apache.sis.internal.storage.MetadataBuilder; +import org.apache.sis.internal.storage.RangeArgument; /** @@ -623,7 +624,7 @@ public final class RasterResource extends AbstractGridResource implements Resour @Override public GridCoverage read(final GridGeometry domain, final int... range) throws DataStoreException { final long startTime = System.nanoTime(); - final RangeArgument rangeIndices = validateRangeArgument(ranges.length, range); + final RangeArgument rangeIndices = RangeArgument.validate(ranges.length, range, listeners); final Variable first = data[bandDimension >= 0 ? 0 : rangeIndices.getFirstSpecified()]; final DataType dataType = first.getDataType(); if (bandDimension < 0) { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java index dca3626..9f8aa04 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java @@ -16,8 +16,6 @@ */ package org.apache.sis.internal.storage; -import java.util.List; -import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.logging.Level; @@ -25,12 +23,9 @@ import java.util.logging.Logger; import java.util.logging.LogRecord; import java.util.concurrent.TimeUnit; import java.math.RoundingMode; -import java.awt.image.ColorModel; -import java.awt.image.SampleModel; import java.awt.image.RasterFormatException; import org.opengis.geometry.Envelope; import org.opengis.metadata.Metadata; -import org.opengis.metadata.spatial.DimensionNameType; import org.opengis.referencing.operation.TransformException; import org.opengis.util.FactoryException; import org.apache.sis.storage.DataStoreException; @@ -42,16 +37,10 @@ import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.DisjointExtentException; -import org.apache.sis.coverage.SampleDimension; -import org.apache.sis.math.MathFunctions; import org.apache.sis.measure.Latitude; import org.apache.sis.measure.Longitude; import org.apache.sis.measure.AngleFormat; -import org.apache.sis.util.ArraysExt; -import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.logging.PerformanceLevel; -import org.apache.sis.internal.coverage.j2d.ColorModelFactory; -import org.apache.sis.internal.coverage.j2d.SampleModelFactory; import org.apache.sis.internal.storage.io.IOUtilities; import org.apache.sis.internal.util.StandardDateFormat; import org.apache.sis.internal.jdk9.JDK9; @@ -71,7 +60,7 @@ import org.apache.sis.internal.jdk9.JDK9; * of the {@link #read(GridGeometry, int...) read(…)} method in subclasses: * * <ul> - * <li>{@link #validateRangeArgument(int, int[])} for validation of the {@code range} argument.</li> + * <li>{@link #canNotRead(String, GridGeometry, Throwable)} for reporting a failure to read operation.</li> * <li>{@link #logReadOperation(Object, GridGeometry, long)} for logging a notice about a read operation.</li> * </ul> * @@ -93,7 +82,7 @@ public abstract class AbstractGridResource extends AbstractResource implements G /** * Returns the grid geometry envelope if known. - * This implementation fetches the envelope from the grid geometry instead of from metadata. + * This implementation fetches the envelope from the grid geometry. * The envelope is absent if the grid geometry does not provide this information. * * @return the grid geometry envelope. @@ -112,6 +101,7 @@ public abstract class AbstractGridResource extends AbstractResource implements G * Invoked in a synchronized block the first time that {@code getMetadata()} is invoked. * The default implementation populates metadata based on information provided by * {@link #getIdentifier() getIdentifier()}, + * {@link #getEnvelope() getEnvelope()}, * {@link #getGridGeometry() getGridGeometry()} and * {@link #getSampleDimensions() getSampleDimensions()}. * Subclasses should override if they can provide more information. @@ -127,352 +117,6 @@ public abstract class AbstractGridResource extends AbstractResource implements G } /** - * Validate the {@code range} argument given to {@link #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 #getSampleDimensions()}.size()</code>. - * @param range the {@code range} argument given by the user. May be null or empty. - * @return the {@code range} argument encapsulated with a set of convenience tools. - * @throws IllegalArgumentException if a range index is invalid. - */ - protected final RangeArgument validateRangeArgument(final int numSampleDimensions, final int[] range) { - ArgumentChecks.ensureStrictlyPositive("numSampleDimensions", numSampleDimensions); - final long[] packed; - if (range == null || range.length == 0) { - packed = new long[numSampleDimensions]; - for (int i=1; i<numSampleDimensions; i++) { - packed[i] = (((long) i) << Integer.SIZE) | i; - } - } else { - /* - * Pattern: [specified `range` value | index in `range` where the value was specified] - */ - packed = new long[range.length]; - for (int i=0; i<range.length; i++) { - final int r = range[i]; - if (r < 0 || r >= numSampleDimensions) { - throw new IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString( - Resources.Keys.InvalidSampleDimensionIndex_2, numSampleDimensions - 1, r)); - } - packed[i] = (((long) r) << Integer.SIZE) | i; - } - /* - * Sort by increasing `range` value, but keep together with index in `range` where each - * value was specified. After sorting, it become easy to check for duplicated values. - */ - Arrays.sort(packed); - int previous = -1; - for (int i=0; i<packed.length; i++) { - // 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( - Resources.Keys.DuplicatedSampleDimensionIndex_1, r)); - } - previous = r; - } - } - return new RangeArgument(packed, packed.length == numSampleDimensions); - } - - /** - * The user-provided {@code range} argument, together with a set of convenience tools. - */ - protected static final class RangeArgument { - /** - * The range indices specified by user in high bits, together (in the low bits) - * with the position in the {@code ranges} array where each index was specified. - * This packing is used for making easier to sort this array in increasing order - * of user-specified range index. - */ - private final long[] packed; - - /** - * Whether the selection contains all bands of the resource, not necessarily in order. - */ - public final boolean hasAllBands; - - /** - * If a {@linkplain #insertSubsampling subsampling} has been applied, indices of the first and last band - * to read, together with the interval (stride) between bands. Those information are computed only when - * the {@code insertFoo(…)} methods are invoked. - * - * @see #insertBandDimension(GridExtent, int) - * @see #insertSubsampling(int[], int) - */ - private int first, last, interval; - - /** - * A builder for sample dimensions, created when first needed. - */ - private SampleDimension.Builder builder; - - /** - * Encapsulates the given {@code range} argument packed in high bits. - */ - private RangeArgument(final long[] packed, final boolean hasAllBands) { - this.packed = packed; - this.hasAllBands = hasAllBands; - this.interval = 1; - } - - /** - * 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. - * - * @return whether user specified all bands in increasing order without subsampling inserted. - */ - public boolean isIdentity() { - if (!hasAllBands || interval != 1) { - return false; - } - for (int i=0; i<packed.length; i++) { - if (packed[i] != ((((long) i) << Integer.SIZE) | i)) { - return false; - } - } - return true; - } - - /** - * Returns the number of sample dimensions. This is the length of the range array supplied by user, - * or the number of bands in the source coverage if the {@code range} array was null or empty. - * - * @return the number of sample dimensions selected by user. - */ - public int getNumBands() { - return packed.length; - } - - /** - * Returns the indices of bands selected by the user. - * This is a copy of the {@code range} argument specified by the user, in same order. - * Note that this is not necessarily increasing order. - * - * @return a copy of the {@code range} argument specified by the user. - */ - public int[] getSelectedBands() { - final int[] bands = new int[getNumBands()]; - for (int i=0; i<bands.length; i++) { - bands[getTargetIndex(i)] = getSourceIndex(i); - } - return bands; - } - - /** - * Returns the value of the first index specified by the user. This is not necessarily equal to - * {@code getSourceIndex(0)} if the user specified the bands out of order. - * - * @return index of the first value in the user-specified {@code range} array. - */ - public int getFirstSpecified() { - for (final long p : packed) { - if (((int) p) == 0) { - return (int) (p >>> Integer.SIZE); - } - } - throw new IllegalStateException(); // Should never happen. - } - - /** - * Returns the i<sup>th</sup> index of the band to read from the resource. - * Indices are returned in strictly increasing order. - * - * @param i index of the range index to get, from 0 inclusive to {@link #getNumBands()} exclusive. - * @return index of the i<sup>th</sup> band to read from the resource. - */ - public int getSourceIndex(final int i) { - return (int) (packed[i] >>> Integer.SIZE); - } - - /** - * Returns the i<sup>th</sup> band position. This is the index in the user-supplied {@code range} array - * where the {@code getSourceIndex(i)} value was specified. - * - * @param i index of the range index to get, from 0 inclusive to {@link #getNumBands()} exclusive. - * @return index in user-supplied {@code range} array where was specified the {@code getSourceIndex(i)} value. - */ - public int getTargetIndex(final int i) { - return (int) packed[i]; - } - - /** - * Returns the i<sup>th</sup> index of the band to read from the resource, after subsampling has been applied. - * The subsampling results from calls to {@link #insertBandDimension(GridExtent, int)} and - * {@link #insertSubsampling(int[], int)} methods. - * - * {@preformat java - * areaOfInterest = rangeIndices.insertBandDimension(areaOfInterest, bandDimension); - * subsampling = rangeIndices.insertSubsampling (subsampling, bandDimension); - * data = myReadMethod(areaOfInterest, subsampling); - * for (int i=0; i<numBands; i++) { - * int bandIndexInTheDataWeJustRead = rangeIndices.getSubsampledIndex(i); - * } - * } - * - * If the {@code insertXXX(…)} methods have never been invoked, then this method is equivalent to {@link #getSourceIndex(int)}. - * - * @param i index of the range index to get, from 0 inclusive to {@link #getNumBands()} exclusive. - * @return index of the i<sup>th</sup> band to read from the resource, after subsampling. - */ - public int getSubsampledIndex(final int i) { - return (getSourceIndex(i) - first) / interval; - } - - /** - * Returns the increment to apply on index for moving to the same band of the next pixel. - * If the {@code insertXXX(…)} methods have never been invoked, then this method returns 1. - * - * @return the increment to apply on index for moving to the next pixel in the same band. - * - * @see java.awt.image.PixelInterleavedSampleModel#getPixelStride() - */ - public int getPixelStride() { - return (last - first) / interval + 1; - } - - /** - * Returns the given extent with a new dimension added for the bands. The extent in the new dimension - * will range from the minimum {@code range} value to the maximum {@code range} value inclusive. - * This method should be used together with {@link #insertSubsampling(int[], int)}. - * - * <h4>Use case</h4> - * This method is useful for reading a <var>n</var>-dimensional data cube with values stored in a - * {@link java.awt.image.PixelInterleavedSampleModel} fashion (except if {@code bandDimension} is - * after all existing {@code areaOfInterest} dimensions, in which case data become organized in a - * {@link java.awt.image.BandedSampleModel} fashion). This method converts the specified domain - * (decomposed in {@code areaOfInterest} and {@code subsampling} parameters) into a larger domain - * encompassing band dimension as if it was an ordinary space or time dimension. It makes possible - * to use this domain with {@link org.apache.sis.internal.storage.io.HyperRectangleReader} for example. - * - * @param areaOfInterest the extent to which to add a new dimension for bands. - * @param bandDimension index of the band dimension. - * @return a new extent with the same values than the given extent plus one dimension for bands. - */ - public GridExtent insertBandDimension(final GridExtent areaOfInterest, final int bandDimension) { - first = getSourceIndex(0); - last = getSourceIndex(packed.length - 1); - return areaOfInterest.insertDimension(bandDimension, DimensionNameType.valueOf("BAND"), first, last, true); - } - - /** - * Returns the given subsampling with a new dimension added for the bands. The subsampling in the new - * dimension will be the greatest common divisor of the difference between all user-specified values. - * This method should be used together with {@link #insertBandDimension(GridExtent, int)}. - * See that method for more information. - * - * <p>Invoking this method changes the values returned by following methods:</p> - * <ul> - * <li>{@link #isIdentity()}</li> - * <li>{@link #getSubsampledIndex(int)}</li> - * <li>{@link #getPixelStride()}</li> - * </ul> - * - * @param subsampling the subsampling to which to add a new dimension for bands. - * @param bandDimension index of the band dimension. - * @return a new subsampling array with the same values than the given array plus one dimension for bands. - */ - public int[] insertSubsampling(int[] subsampling, final int bandDimension) { - final int[] delta = new int[packed.length - 1]; - for (int i=0; i<delta.length; i++) { - delta[i] = getSourceIndex(i+1) - getSourceIndex(i); - } - final int[] divisors = MathFunctions.commonDivisors(delta); - interval = (divisors.length != 0) ? divisors[divisors.length - 1] : 1; - subsampling = ArraysExt.insert(subsampling, bandDimension, 1); - subsampling[bandDimension] = interval; - return subsampling; - } - - /** - * Returns sample dimensions selected by the user. This is a convenience method for situations where - * sample dimensions are already in memory and there is no advantage to read them in "physical" order. - * - * @param sourceBands bands in the source coverage. - * @return bands selected by user, in user-specified order. - */ - public SampleDimension[] select(final List<? extends SampleDimension> sourceBands) { - final SampleDimension[] bands = new SampleDimension[getNumBands()]; - for (int i=0; i<bands.length; i++) { - bands[getTargetIndex(i)] = sourceBands.get(getSourceIndex(i)); - } - return bands; - } - - /** - * Returns a sample model for the bands specified by the user. - * The model created by this method can be a "view" or can be "compressed": - * - * <ul class="verbose"> - * <li>If {@code view} is {@code true}, the sample model returned by this method will expect the - * same {@link java.awt.image.DataBuffer} than the one expected by the original {@code model}. - * Bands enumerated in the {@code range} argument will be used and other bands will be ignored. - * This mode is efficient if the data are already in memory and we want to avoid copying them. - * An inconvenient is that all bands, including the ignored ones, are retained in memory.</li> - * <li>If {@code view} is {@code false}, then this method will "compress" bank indices and bit masks - * for making them consecutive. For example if the {@code range} argument specifies that the bands - * to read are {1, 3, 4, 6, …}, then "compressed" sample model will use bands {0, 1, 2, 3, …}. - * This mode is efficient if the data are not yet in memory and the reader is capable to skip - * the bands to ignore. In such case, this mode save memory.</li> - * </ul> - * - * @param model the original sample model with all bands. Can be {@code null}. - * @param view whether the band subset shall be a view over the full band set. - * @return the sample model for a subset of bands, or {@code null} if the given sample model was null. - * @throws RasterFormatException if the given sample model is not recognized. - * @throws IllegalArgumentException if an error occurred when constructing the new sample model. - * - * @see SampleModel#createSubsetSampleModel(int[]) - * @see SampleModelFactory#subsetAndCompress(int[]) - */ - public SampleModel select(final SampleModel model, final boolean view) { - if (model == null || isIdentity()) { - return model; - } - final int[] bands = getSelectedBands(); - if (view) { - return model.createSubsetSampleModel(bands); - } else { - final SampleModelFactory factory = new SampleModelFactory(model); - factory.subsetAndCompress(bands); - return factory.build(); - } - } - - /** - * Returns a color model for the bands specified by the user. - * - * @param colors the original color model with all bands. Can be {@code null}. - * @return the color model for a subset of bands, or {@code null} if the given color model was null. - */ - public ColorModel select(final ColorModel colors) { - if (colors == null || isIdentity()) { - return colors; - } - return ColorModelFactory.createSubset(colors, getSelectedBands()) - .orElse(null); - } - - /** - * Returns a builder for sample dimensions. This method recycles the same builder on every calls. - * If the builder has been returned by a previous call to this method, - * then it is {@linkplain SampleDimension.Builder#clear() cleared} before to be returned again. - * - * @return a recycled builder for sample dimensions. - */ - public SampleDimension.Builder builder() { - if (builder == null) { - builder = new SampleDimension.Builder(); - } else { - builder.clear(); - } - return builder; - } - } - - /** * Creates an exception for a failure to load data. If the failure may be caused by an envelope * outside the resource domain, that envelope will be inferred from the {@code request} argument. * @@ -526,9 +170,9 @@ public abstract class AbstractGridResource extends AbstractResource implements G * The log level will be {@link Level#FINE} if the operation was quick enough, * or {@link PerformanceLevel#SLOW} or higher level otherwise. * - * @param file the file that was opened, or {@code null} for {@link #getSourceName()}. - * @param domain domain of the created grid coverage. - * @param startTime value of {@link System#nanoTime()} when the loading process started. + * @param file the file that was opened, or {@code null} for {@link #getSourceName()}. + * @param domain domain of the created grid coverage. + * @param startTime value of {@link System#nanoTime()} when the loading process started. */ protected final void logReadOperation(final Object file, final GridGeometry domain, final long startTime) { final Logger logger = listeners.getLogger(); diff --git 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 index 706680e..7b85d75 100644 --- 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 @@ -94,7 +94,7 @@ public class MemoryGridResource extends AbstractGridResource { @Override public GridCoverage read(GridGeometry domain, final int... range) { List<SampleDimension> bands = coverage.getSampleDimensions(); - final RangeArgument rangeIndices = validateRangeArgument(bands.size(), range); + final RangeArgument rangeIndices = RangeArgument.validate(bands.size(), range, listeners); /* * The given `domain` may use arbitrary `gridToCRS` and `CRS` properties. * For this simple implementation we need the same `gridToCRS` and `CRS` diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/RangeArgument.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/RangeArgument.java new file mode 100644 index 0000000..4e84330 --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/RangeArgument.java @@ -0,0 +1,386 @@ +/* + * 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.storage; + +import java.util.List; +import java.util.Arrays; +import java.awt.image.ColorModel; +import java.awt.image.SampleModel; +import java.awt.image.RasterFormatException; +import org.opengis.metadata.spatial.DimensionNameType; +import org.apache.sis.coverage.SampleDimension; +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.math.MathFunctions; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.Localized; + + +/** + * The user-provided {@code range} argument together with a set of convenience tools. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 0.8 + * @module + */ +public final class RangeArgument { + /** + * The range indices specified by user in high bits, together (in the low bits) + * with the position in the {@code ranges} array where each index was specified. + * This packing is used for making easier to sort this array in increasing order + * of user-specified range index. + */ + private final long[] packed; + + /** + * Whether the selection contains all bands of the resource, not necessarily in order. + */ + public final boolean hasAllBands; + + /** + * If a {@linkplain #insertSubsampling subsampling} has been applied, indices of the first and last band + * to read, together with the interval (stride) between bands. Those information are computed only when + * the {@code insertFoo(…)} methods are invoked. + * + * @see #insertBandDimension(GridExtent, int) + * @see #insertSubsampling(int[], int) + */ + private int first, last, interval; + + /** + * A builder for sample dimensions, created when first needed. + */ + private SampleDimension.Builder builder; + + /** + * Encapsulates the given {@code range} argument packed in high bits. + */ + private RangeArgument(final long[] packed, final boolean hasAllBands) { + this.packed = packed; + this.hasAllBands = hasAllBands; + this.interval = 1; + } + + /** + * Validate the {@code range} 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. + * + * @param numSampleDimensions number of sample dimensions in the resource. + * Equal to <code>{@linkplain GridCoverageResource#getSampleDimensions()}.size()</code>. + * @param range the {@code range} argument given by the user. May be null or empty. + * @param listeners source of locale to use if an exception must be thrown. + * @return the {@code range} argument encapsulated with a set of convenience tools. + * @throws IllegalArgumentException if a range index is invalid. + */ + public static RangeArgument validate(final int numSampleDimensions, final int[] range, final Localized listeners) { + ArgumentChecks.ensureStrictlyPositive("numSampleDimensions", numSampleDimensions); + final long[] packed; + if (range == null || range.length == 0) { + packed = new long[numSampleDimensions]; + for (int i=1; i<numSampleDimensions; i++) { + packed[i] = (((long) i) << Integer.SIZE) | i; + } + } else { + /* + * Pattern: [specified `range` value | index in `range` where the value was specified] + */ + packed = new long[range.length]; + for (int i=0; i<range.length; i++) { + final int r = range[i]; + if (r < 0 || r >= numSampleDimensions) { + throw new IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString( + Resources.Keys.InvalidSampleDimensionIndex_2, numSampleDimensions - 1, r)); + } + packed[i] = (((long) r) << Integer.SIZE) | i; + } + /* + * Sort by increasing `range` value, but keep together with index in `range` where each + * value was specified. After sorting, it become easy to check for duplicated values. + */ + Arrays.sort(packed); + int previous = -1; + for (int i=0; i<packed.length; i++) { + // 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( + Resources.Keys.DuplicatedSampleDimensionIndex_1, r)); + } + previous = r; + } + } + return new RangeArgument(packed, packed.length == numSampleDimensions); + } + + /** + * 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. + * + * @return whether user specified all bands in increasing order without subsampling inserted. + */ + public boolean isIdentity() { + if (!hasAllBands || interval != 1) { + return false; + } + for (int i=0; i<packed.length; i++) { + if (packed[i] != ((((long) i) << Integer.SIZE) | i)) { + return false; + } + } + return true; + } + + /** + * Returns the number of sample dimensions. This is the length of the range array supplied by user, + * or the number of bands in the source coverage if the {@code range} array was null or empty. + * + * @return the number of sample dimensions selected by user. + */ + public int getNumBands() { + return packed.length; + } + + /** + * Returns the indices of bands selected by the user. + * This is a copy of the {@code range} argument specified by the user, in same order. + * Note that this is not necessarily increasing order. + * + * @return a copy of the {@code range} argument specified by the user. + */ + public int[] getSelectedBands() { + final int[] bands = new int[getNumBands()]; + for (int i=0; i<bands.length; i++) { + bands[getTargetIndex(i)] = getSourceIndex(i); + } + return bands; + } + + /** + * Returns the value of the first index specified by the user. This is not necessarily equal to + * {@code getSourceIndex(0)} if the user specified the bands out of order. + * + * @return index of the first value in the user-specified {@code range} array. + */ + public int getFirstSpecified() { + for (final long p : packed) { + if (((int) p) == 0) { + return (int) (p >>> Integer.SIZE); + } + } + throw new IllegalStateException(); // Should never happen. + } + + /** + * Returns the i<sup>th</sup> index of the band to read from the resource. + * Indices are returned in strictly increasing order. + * + * @param i index of the range index to get, from 0 inclusive to {@link #getNumBands()} exclusive. + * @return index of the i<sup>th</sup> band to read from the resource. + */ + public int getSourceIndex(final int i) { + return (int) (packed[i] >>> Integer.SIZE); + } + + /** + * Returns the i<sup>th</sup> band position. This is the index in the user-supplied {@code range} array + * where the {@code getSourceIndex(i)} value was specified. + * + * @param i index of the range index to get, from 0 inclusive to {@link #getNumBands()} exclusive. + * @return index in user-supplied {@code range} array where was specified the {@code getSourceIndex(i)} value. + */ + public int getTargetIndex(final int i) { + return (int) packed[i]; + } + + /** + * Returns the i<sup>th</sup> index of the band to read from the resource, after subsampling has been applied. + * The subsampling results from calls to {@link #insertBandDimension(GridExtent, int)} and + * {@link #insertSubsampling(int[], int)} methods. + * + * {@preformat java + * areaOfInterest = rangeIndices.insertBandDimension(areaOfInterest, bandDimension); + * subsampling = rangeIndices.insertSubsampling (subsampling, bandDimension); + * data = myReadMethod(areaOfInterest, subsampling); + * for (int i=0; i<numBands; i++) { + * int bandIndexInTheDataWeJustRead = rangeIndices.getSubsampledIndex(i); + * } + * } + * + * If the {@code insertXXX(…)} methods have never been invoked, then this method is equivalent to {@link #getSourceIndex(int)}. + * + * @param i index of the range index to get, from 0 inclusive to {@link #getNumBands()} exclusive. + * @return index of the i<sup>th</sup> band to read from the resource, after subsampling. + */ + public int getSubsampledIndex(final int i) { + return (getSourceIndex(i) - first) / interval; + } + + /** + * Returns the increment to apply on index for moving to the same band of the next pixel. + * If the {@code insertXXX(…)} methods have never been invoked, then this method returns 1. + * + * @return the increment to apply on index for moving to the next pixel in the same band. + * + * @see java.awt.image.PixelInterleavedSampleModel#getPixelStride() + */ + public int getPixelStride() { + return (last - first) / interval + 1; + } + + /** + * Returns the given extent with a new dimension added for the bands. The extent in the new dimension + * will range from the minimum {@code range} value to the maximum {@code range} value inclusive. + * This method should be used together with {@link #insertSubsampling(int[], int)}. + * + * <h4>Use case</h4> + * This method is useful for reading a <var>n</var>-dimensional data cube with values stored in a + * {@link java.awt.image.PixelInterleavedSampleModel} fashion (except if {@code bandDimension} is + * after all existing {@code areaOfInterest} dimensions, in which case data become organized in a + * {@link java.awt.image.BandedSampleModel} fashion). This method converts the specified domain + * (decomposed in {@code areaOfInterest} and {@code subsampling} parameters) into a larger domain + * encompassing band dimension as if it was an ordinary space or time dimension. It makes possible + * to use this domain with {@link org.apache.sis.internal.storage.io.HyperRectangleReader} for example. + * + * @param areaOfInterest the extent to which to add a new dimension for bands. + * @param bandDimension index of the band dimension. + * @return a new extent with the same values than the given extent plus one dimension for bands. + */ + public GridExtent insertBandDimension(final GridExtent areaOfInterest, final int bandDimension) { + first = getSourceIndex(0); + last = getSourceIndex(packed.length - 1); + return areaOfInterest.insertDimension(bandDimension, DimensionNameType.valueOf("BAND"), first, last, true); + } + + /** + * Returns the given subsampling with a new dimension added for the bands. The subsampling in the new + * dimension will be the greatest common divisor of the difference between all user-specified values. + * This method should be used together with {@link #insertBandDimension(GridExtent, int)}. + * See that method for more information. + * + * <p>Invoking this method changes the values returned by following methods:</p> + * <ul> + * <li>{@link #isIdentity()}</li> + * <li>{@link #getSubsampledIndex(int)}</li> + * <li>{@link #getPixelStride()}</li> + * </ul> + * + * @param subsampling the subsampling to which to add a new dimension for bands. + * @param bandDimension index of the band dimension. + * @return a new subsampling array with the same values than the given array plus one dimension for bands. + */ + public int[] insertSubsampling(int[] subsampling, final int bandDimension) { + final int[] delta = new int[packed.length - 1]; + for (int i=0; i<delta.length; i++) { + delta[i] = getSourceIndex(i+1) - getSourceIndex(i); + } + final int[] divisors = MathFunctions.commonDivisors(delta); + interval = (divisors.length != 0) ? divisors[divisors.length - 1] : 1; + subsampling = ArraysExt.insert(subsampling, bandDimension, 1); + subsampling[bandDimension] = interval; + return subsampling; + } + + /** + * Returns sample dimensions selected by the user. This is a convenience method for situations where + * sample dimensions are already in memory and there is no advantage to read them in "physical" order. + * + * @param sourceBands bands in the source coverage. + * @return bands selected by user, in user-specified order. + */ + public SampleDimension[] select(final List<? extends SampleDimension> sourceBands) { + final SampleDimension[] bands = new SampleDimension[getNumBands()]; + for (int i=0; i<bands.length; i++) { + bands[getTargetIndex(i)] = sourceBands.get(getSourceIndex(i)); + } + return bands; + } + + /** + * Returns a sample model for the bands specified by the user. + * The model created by this method can be a "view" or can be "compressed": + * + * <ul class="verbose"> + * <li>If {@code view} is {@code true}, the sample model returned by this method will expect the + * same {@link java.awt.image.DataBuffer} than the one expected by the original {@code model}. + * Bands enumerated in the {@code range} argument will be used and other bands will be ignored. + * This mode is efficient if the data are already in memory and we want to avoid copying them. + * An inconvenient is that all bands, including the ignored ones, are retained in memory.</li> + * <li>If {@code view} is {@code false}, then this method will "compress" bank indices and bit masks + * for making them consecutive. For example if the {@code range} argument specifies that the bands + * to read are {1, 3, 4, 6, …}, then "compressed" sample model will use bands {0, 1, 2, 3, …}. + * This mode is efficient if the data are not yet in memory and the reader is capable to skip + * the bands to ignore. In such case, this mode save memory.</li> + * </ul> + * + * @param model the original sample model with all bands. Can be {@code null}. + * @param view whether the band subset shall be a view over the full band set. + * @return the sample model for a subset of bands, or {@code null} if the given sample model was null. + * @throws RasterFormatException if the given sample model is not recognized. + * @throws IllegalArgumentException if an error occurred when constructing the new sample model. + * + * @see SampleModel#createSubsetSampleModel(int[]) + * @see SampleModelFactory#subsetAndCompress(int[]) + */ + public SampleModel select(final SampleModel model, final boolean view) { + if (model == null || isIdentity()) { + return model; + } + final int[] bands = getSelectedBands(); + if (view) { + return model.createSubsetSampleModel(bands); + } else { + final SampleModelFactory factory = new SampleModelFactory(model); + factory.subsetAndCompress(bands); + return factory.build(); + } + } + + /** + * Returns a color model for the bands specified by the user. + * + * @param colors the original color model with all bands. Can be {@code null}. + * @return the color model for a subset of bands, or {@code null} if the given color model was null. + */ + public ColorModel select(final ColorModel colors) { + if (colors == null || isIdentity()) { + return colors; + } + return ColorModelFactory.createSubset(colors, getSelectedBands()) + .orElse(null); + } + + /** + * Returns a builder for sample dimensions. This method recycles the same builder on every calls. + * If the builder has been returned by a previous call to this method, + * then it is {@linkplain SampleDimension.Builder#clear() cleared} before to be returned again. + * + * @return a recycled builder for sample dimensions. + */ + public SampleDimension.Builder builder() { + if (builder == null) { + builder = new SampleDimension.Builder(); + } else { + builder.clear(); + } + return builder; + } +} diff --git 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 index 9f8b5cd..0baf6b3 100644 --- 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 @@ -328,7 +328,7 @@ public abstract class TiledGridResource extends AbstractGridResource { */ public Subset(GridGeometry domain, final int[] range) throws DataStoreException { List<SampleDimension> bands = getSampleDimensions(); - final RangeArgument rangeIndices = validateRangeArgument(bands.size(), range); + final RangeArgument rangeIndices = RangeArgument.validate(bands.size(), range, listeners); final GridGeometry gridGeometry = getGridGeometry(); sourceExtent = gridGeometry.getExtent(); tileSize = getTileSize(); diff --git 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 index 2a6ab36..d8c7ff0 100644 --- 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 @@ -40,7 +40,7 @@ import static org.apache.sis.test.Assert.*; * @since 1.1 * @module */ -@DependsOn(AbstractGridResourceTest.class) +@DependsOn(RangeArgumentTest.class) public final strictfp class MemoryGridResourceTest extends TestCase { /** * Arbitrary size for the grid to test. diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java similarity index 88% rename from storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java rename to storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java index 11b85cd..5cc7437 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java @@ -17,25 +17,27 @@ package org.apache.sis.internal.storage; 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.test.TestCase; +import org.apache.sis.util.Localized; import org.junit.Test; import static org.junit.Assert.*; /** - * Tests {@link AbstractGridResource} and {@link AbstractGridResource.RangeArgument}. + * Tests {@link RangeArgument}. * * @author Martin Desruisseaux (Geomatys) * @version 1.2 * @since 1.0 * @module */ -public final strictfp class AbstractGridResourceTest extends TestCase { +public final strictfp class RangeArgumentTest extends TestCase implements Localized { /** * A resource performing no operation. */ @@ -46,12 +48,22 @@ public final strictfp class AbstractGridResourceTest extends TestCase { }; /** + * Returns a fixed locale for testing purpose. + * + * @return a fixed locale. + */ + @Override + public Locale getLocale() { + return Locale.US; + } + + /** * Tests {@link AbstractGridResource.RangeArgument} for data organized in a banded sample model. * This is the state when no {@code insert} method is invoked. */ @Test public void testRangeArgumentForBandedModel() { - final AbstractGridResource.RangeArgument r = resource.validateRangeArgument(7, new int[] {4, 6, 2}); + final RangeArgument r = RangeArgument.validate(7, new int[] {4, 6, 2}, this); assertEquals("numBands", 3, r.getNumBands()); assertEquals("first", 4, r.getFirstSpecified()); assertEquals("source", 2, r.getSourceIndex(0)); // Expect sorted source indices: {2, 4, 6}. @@ -72,7 +84,7 @@ public final strictfp class AbstractGridResourceTest extends TestCase { */ @Test public void testRangeArgumentForInterleavedModel() { - final AbstractGridResource.RangeArgument r = resource.validateRangeArgument(7, new int[] {4, 6, 2}); + final RangeArgument r = RangeArgument.validate(7, new int[] {4, 6, 2}, this); assertEquals(3, r.insertBandDimension(new GridExtent(360, 180), 2).getDimension()); assertArrayEquals(new int[] {3, 1, 2}, r.insertSubsampling(new int[] {3, 1}, 2)); assertEquals("numBands", 3, r.getNumBands()); diff --git 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 index a638d5d..1703a23 100644 --- 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 @@ -41,7 +41,7 @@ import org.junit.BeforeClass; org.apache.sis.internal.storage.io.HyperRectangleReaderTest.class, org.apache.sis.internal.storage.io.RewindableLineReaderTest.class, org.apache.sis.internal.storage.MetadataBuilderTest.class, - org.apache.sis.internal.storage.AbstractGridResourceTest.class, + org.apache.sis.internal.storage.RangeArgumentTest.class, org.apache.sis.internal.storage.MemoryGridResourceTest.class, org.apache.sis.storage.FeatureNamingTest.class, org.apache.sis.storage.ProbeResultTest.class,
