| /* |
| * 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.privy; |
| |
| import java.util.List; |
| import java.util.Arrays; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.IdentityHashMap; |
| import java.util.Objects; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| import java.util.function.ToIntFunction; |
| import java.lang.reflect.Array; |
| import org.apache.sis.coverage.grid.PixelInCell; |
| import org.apache.sis.coverage.SampleDimension; |
| import org.apache.sis.coverage.grid.GridGeometry; |
| import org.apache.sis.coverage.grid.IllegalGridGeometryException; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.privy.Numerics; |
| import org.apache.sis.util.resources.Errors; |
| |
| |
| /** |
| * Helper class for building a combined domain or range from aggregated sources. |
| * This helper class is shared for aggregation operations on different sources: |
| * rendered images, grid coverages and resources. |
| * |
| * <p>Instances of this class should be short-lived. |
| * They are used only the time needed for constructing an image or coverage operation.</p> |
| * |
| * <p>This class can optionally verify if some sources are themselves aggregated images or coverages. |
| * This is done by an {@link #unwrap(Consumer)}, which should be invoked in order to get a flattened |
| * view of nested aggregations.</p> |
| * |
| * <p>All methods in this class may return direct references to internal arrays. |
| * This is okay if instances of this class are discarded immediately after usage.</p> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * |
| * @param <S> type of objects that are the source of sample dimensions. |
| */ |
| @SuppressWarnings("ReturnOfCollectionOrArrayField") // See class Javadoc. |
| public final class MultiSourceArgument<S> { |
| /** |
| * The user-specified sources, usually grid coverages or rendered images. |
| * This is initially a copy of the array specified at construction time. |
| * This array is modified in-place by {@code validate(…)} methods for |
| * removing empty sources and flattening nested aggregations. |
| * |
| * @see #sources() |
| */ |
| private S[] sources; |
| |
| /** |
| * Indices of selected bands or sample dimensions for each source. |
| * This array is modified in-place by {@code validate(…)} methods |
| * for removing empty elements and flattening nested aggregations. |
| * |
| * The length of this array must be always equal to the {@link #sources} array length. |
| * The array is non-null but may contain {@code null} elements for meaning "all bands" |
| * before validation. After validation, all null elements are replaced by sequences. |
| * |
| * @see #bandsPerSource(boolean) |
| */ |
| private int[][] bandsPerSource; |
| |
| /** |
| * Number of bands for each source source. This information is necessary |
| * for determining whether a selection of bands is an identity operation. |
| * |
| * <p>This field is initially null and assigned on validation. |
| * Consequently this field can also be used for checking whether |
| * one of the {@code validate(…)} methods has been invoked.</p> |
| * |
| * @see #completeAndValidate(Function) |
| * @see #validate(ToIntFunction) |
| */ |
| private int[] numBandsPerSource; |
| |
| /** |
| * Number of valid elements in {@link #sources} array after empty elements have been removed. |
| * This is initially zero and is set after a {@code validate(…)} method has been invoked. |
| */ |
| private int validatedSourceCount; |
| |
| /** |
| * Total number of bands. This is the length of the {@link #ranges} list, |
| * except that this information is provided even if {@code ranges} is null. |
| */ |
| private int totalBandCount; |
| |
| /** |
| * Union of all selected bands in all specified sources, or {@code null} if not applicable. |
| */ |
| private List<SampleDimension> ranges; |
| |
| /** |
| * Translations in units of grid cells to apply for obtaining a grid geometry |
| * compatible with the "grid to CRS" transform of a source. |
| */ |
| private long[][] gridTranslations; |
| |
| /** |
| * Index of a source having the same "grid to CRS" transform than the grid geometry |
| * returned by {@link #domain(Function)}. If there is none, then this value is -1. |
| */ |
| private int sourceOfGridToCRS = -1; |
| |
| /** |
| * A method which may decompose a source in a sequence of deeper sources associated with their bands to select. |
| * Shall be set (if desired) before a {@code validate(…)} method is invoked. |
| * |
| * @see #unwrap(Consumer) |
| */ |
| private Consumer<Unwrapper> unwrapper; |
| |
| /** |
| * Prepares an argument validator for the given sources and bands arguments. |
| * The optional {@code bandsPerSource} argument specifies the bands to select in each source images. |
| * That array can be {@code null} for selecting all bands in all source images, |
| * or may contain {@code null} elements for selecting all bands of the corresponding image. |
| * An empty array element (i.e. zero band to select) discards the corresponding source image. |
| * |
| * <p>One of the {@code validate(…)} method shall be invoked after this constructor.</p> |
| * |
| * @param sources the sources from which to get the sample dimensions. |
| * @param bandsPerSource sample dimensions for each source. May contain {@code null} elements. |
| */ |
| public MultiSourceArgument(S[] sources, int[][] bandsPerSource) { |
| /* |
| * Ensure that both arrays are non-null and have the same length. |
| * Copy those arrays because their content will be overwritten. |
| */ |
| ArgumentChecks.ensureNonEmpty("sources", sources); |
| final int n = sources.length; |
| if (bandsPerSource != null) { |
| if (bandsPerSource.length > n) { |
| throw new IllegalArgumentException(Errors.format( |
| Errors.Keys.TooManyCollectionElements_3, |
| "bandsPerSource", bandsPerSource.length, n)); |
| } |
| bandsPerSource = Arrays.copyOf(bandsPerSource, n); |
| } else { |
| bandsPerSource = new int[n][]; |
| } |
| this.sources = sources.clone(); |
| this.bandsPerSource = bandsPerSource; |
| } |
| |
| /** |
| * Ensures that a {@code validate(…)} method has been invoked (or not). |
| * |
| * @param expected {@code true} if the caller expects validation to be done, or |
| * {@code false} if the caller expects validation to not be done yet. |
| */ |
| private void checkValidationState(final boolean expected) { |
| if ((numBandsPerSource == null) == expected) { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /** |
| * Specifies a method which, given a source, may decompose that source |
| * in a sequence of deeper sources associated with their bands to select. |
| * The consumer will be invoked for all sources specified to the constructor. |
| * If a source can be decomposed, then the specified consumer should invoke |
| * {@code apply(…)} on the given {@code Unwrapper} instance. |
| * |
| * @param filter the method to invoke for getting the sources of an image or coverage. |
| */ |
| public void unwrap(final Consumer<Unwrapper> filter) { |
| checkValidationState(false); |
| unwrapper = filter; |
| } |
| |
| /** |
| * Asks to the {@linkplain #unwrapper} if the given source can be decomposed into deeper sources. |
| * |
| * @param index index of {@code source} in the {@link #sources} array. |
| * @param source the source to potentially unwrap. |
| * @param bands the bands to use in the source. Shall not be {@code null}. |
| * @return whether the source has been decomposed. |
| */ |
| private boolean unwrap(int index, S source, int[] bands) { |
| if (unwrapper == null) { |
| return false; |
| } |
| final Unwrapper handler = new Unwrapper(index, source, bands); |
| unwrapper.accept(handler); |
| return handler.done; |
| } |
| |
| /** |
| * Replace a user supplied source by a deeper source with the bands to select. |
| * This is used for getting a flattened view of nested aggregations. |
| */ |
| public final class Unwrapper { |
| /** |
| * Index of {@link #source} in the {@link #sources} array. |
| */ |
| private final int index; |
| |
| /** |
| * The source to potentially unwrap. |
| */ |
| public final S source; |
| |
| /** |
| * The bands to use in the source (never {@code null}). |
| * This array shall not modified because it may be a reference to an internal array. |
| */ |
| public final int[] bands; |
| |
| /** |
| * Whether the source has been decomposed in deeper sources. |
| */ |
| private boolean done; |
| |
| /** |
| * Creates a new instance to be submitted to user supplied {@link #unwrapper}. |
| */ |
| private Unwrapper(final int index, final S source, final int[] bands) { |
| this.index = index; |
| this.source = source; |
| this.bands = bands; |
| } |
| |
| /** |
| * Invoke {@code apply(…)} with components that are a subset of an existing aggregate. |
| * This is a helper method for decomposing an aggregate into its component. |
| * |
| * @param sources all sources of the aggregate to decompose. |
| * @param bandsPerSource selected bands of the aggregate to decompose. May contain null elements. |
| * @param getter same getter as {@link #completeAndValidate(Function)}, used for getting the number of bands. |
| */ |
| public void applySubset(final S[] sources, final int[][] bandsPerSource, |
| final Function<S, List<SampleDimension>> getter) |
| { |
| @SuppressWarnings("unchecked") |
| final S[] components = (S[]) Array.newInstance(sources.getClass().getComponentType(), bands.length); |
| final int[][] componentBands = new int[bands.length][]; |
| |
| int sourceIndex = -1; |
| int[] sourceBands = null; // Value of `bandsPerSource[sourceIndex]`. |
| S component = null; // Value of `sources[sourceIndex]` potentially used as component. |
| int lower=0, upper=0; // Range of band indices in which `component` is valid. |
| for (int i=0; i<bands.length; i++) { |
| int band = bands[i]; |
| if (band < lower) { |
| lower = upper = 0; |
| sourceIndex = -1; |
| } |
| while (band >= upper) { |
| component = sources[++sourceIndex]; |
| sourceBands = bandsPerSource[sourceIndex]; |
| lower = upper; |
| upper += (sourceBands != null) ? sourceBands.length : getter.apply(component).size(); |
| } |
| band -= lower; |
| if (sourceBands != null) { |
| band = sourceBands[band]; |
| } |
| componentBands[i] = new int[] {band}; |
| components[i] = component; |
| } |
| /* |
| * Tne same component may be repeated many times in the `sources` array, each time with only one band specified. |
| * We rely on the encloding class post-processing for merging multiple references to a single one for each source. |
| */ |
| apply(components, componentBands); |
| } |
| |
| /** |
| * Notifies the enclosing {@code MultiSourceArgument} that the {@linkplain #source} |
| * shall be replaced by deeper sources. The {@code componentBands} array specifies |
| * the bands to use for each source and shall take in account the {@link #bands} subset. |
| * |
| * @param components the deeper sources to use in replacement to {@link #source}. |
| * @param componentBands the bands to use in replacement for {@link #bands}. |
| */ |
| public void apply(final S[] components, final int[][] componentBands) { |
| final int n = components.length; |
| if (componentBands.length != n) { |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedArrayLengths)); |
| } |
| if (done) throw new IllegalStateException(); |
| sources = ArraysExt.insert(sources, index+1, n-1); |
| bandsPerSource = ArraysExt.insert(bandsPerSource, index+1, n-1); |
| System.arraycopy(components, 0, sources, index, n); |
| System.arraycopy(componentBands, 0, bandsPerSource, index, n); |
| done = true; |
| } |
| } |
| |
| /** |
| * Validates the arguments given to the constructor. |
| * |
| * @param counter method to invoke for counting the number of bands in a source. |
| * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity. |
| */ |
| public void validate(final ToIntFunction<S> counter) { |
| checkValidationState(false); |
| validate(null, Objects.requireNonNull(counter)); |
| } |
| |
| /** |
| * Computes the union of bands in the source given at construction time, then validates. |
| * The union of bands is stored in {@link #ranges()}. |
| * |
| * @param getter method to invoke for getting the list of sample dimensions. |
| * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity. |
| */ |
| public void completeAndValidate(final Function<S, List<SampleDimension>> getter) { |
| checkValidationState(false); |
| ranges = new ArrayList<>(); |
| validate(Objects.requireNonNull(getter), null); |
| } |
| |
| /** |
| * Clones and validates the arguments given to the constructor. |
| * This method ensures that all band indices are in their ranges of validity with no duplicated value. |
| * Then this method stores a copy of the band indices, replacing {@code null} values by sequences. |
| * If an empty array of bands is specified, then the corresponding source is omitted. |
| * |
| * <p>Exactly one of {@code getter} or {@code counter} arguments shall be non-null.</p> |
| * |
| * @param getter method to invoke for getting the list of sample dimensions. |
| * @param counter method to invoke for counting the number of bands in a source. |
| * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity. |
| */ |
| private void validate(final Function<S, List<SampleDimension>> getter, final ToIntFunction<S> counter) { |
| final HashMap<Integer,int[]> identityPool = new HashMap<>(); |
| numBandsPerSource = new int[sources.length]; |
| next: for (int i=0; i<sources.length; i++) { // `sources.length` may change during the loop. |
| S source; |
| int[] selected; |
| List<SampleDimension> sourceBands; |
| int numSourceBands; |
| RangeArgument range; |
| do { |
| selected = bandsPerSource[i]; |
| if (selected != null && selected.length == 0) { |
| // Note that the source is allowed to be null in this particular case. |
| continue next; |
| } |
| source = sources[i]; |
| ArgumentChecks.ensureNonNullElement("sources", i, source); |
| if (getter != null) { |
| sourceBands = getter.apply(source); |
| numSourceBands = sourceBands.size(); |
| } else { |
| sourceBands = null; |
| numSourceBands = counter.applyAsInt(source); |
| } |
| range = RangeArgument.validate(numSourceBands, selected, null); |
| selected = range.getSelectedBands(); |
| /* |
| * Verify if the source is a nested aggregation, in order to get a flattened view. |
| * This replacement must be done before the check for duplicated image references. |
| * The call to `unwrap` may result in a need to grow `numBandsPerSource` array. |
| */ |
| } while (unwrap(i, source, selected)); |
| /* |
| * Now that the arguments have been validated, overwrite the array elements. |
| * The new values may be written at an index lower than `i` if some empty |
| * sources have been excluded. |
| */ |
| if (validatedSourceCount >= numBandsPerSource.length) { |
| // Needed if `unwrap(source)` has expanded the sources array. |
| numBandsPerSource = Arrays.copyOf(numBandsPerSource, sources.length); |
| } |
| if (ranges != null) { |
| for (int j : selected) { |
| ranges.add(sourceBands.get(j)); |
| } |
| } |
| if (range.isIdentity()) { |
| int[] previous = identityPool.putIfAbsent(numSourceBands, selected); |
| if (previous != null) selected = previous; |
| } |
| sources [validatedSourceCount] = source; |
| bandsPerSource [validatedSourceCount] = selected; |
| numBandsPerSource[validatedSourceCount] = numSourceBands; |
| totalBandCount += range.getNumBands(); |
| validatedSourceCount++; |
| } |
| } |
| |
| /** |
| * For each source which is repeated in consecutive positions, merges the repetition in a single reference. |
| * This method does the same work as {@link #mergeDuplicatedSources()}, except that it is restricted to |
| * repetitions in consecutive positions. Because of this restriction, the band order is never modified by |
| * this method call. |
| */ |
| public void mergeConsecutiveSources() { |
| checkValidationState(true); |
| for (int i=1; i < validatedSourceCount;) { |
| if (sources[i] == sources[i-1]) { |
| bandsPerSource[i-1] = ArraysExt.concatenate(bandsPerSource[i-1], bandsPerSource[i]); |
| final int remaining = --validatedSourceCount - i; |
| System.arraycopy(sources, i+1, sources, i, remaining); |
| System.arraycopy(bandsPerSource, i+1, bandsPerSource, i, remaining); |
| System.arraycopy(numBandsPerSource, i+1, numBandsPerSource, i, remaining); |
| } else { |
| i++; |
| } |
| } |
| } |
| |
| /** |
| * If the same sources are repeated many times, merges each repetition in a single reference. |
| * The {@link #sources()} and {@link #bandsPerSource(boolean)} values are modified in-place. |
| * The bands associated to each source reference are merged together, but not necessarily in the same order. |
| * Caller must perform a "band select" operation using the array returned by this method |
| * in order to reconstitute the band order specified by the user. |
| * |
| * <p>This method does the same work as {@link #mergeConsecutiveSources()} except that this method can merge |
| * sources that are not necessarily at consecutive positions. The sources can be repeated at random positions. |
| * But the cost of this flexibility is the possible modification of band order.</p> |
| * |
| * <h4>Use cases</h4> |
| * {@code BandAggregateImage.subset(…)} and |
| * {@code BandAggregateGridResource.read(…)} |
| * implementations rely on this optimization. |
| * |
| * @return the bands to specify in a "band select" operation for reconstituting the user-specified band order. |
| */ |
| public int[] mergeDuplicatedSources() { |
| checkValidationState(true); |
| /* |
| * Merge together the bands of all sources that are repeated. |
| * The band indices are stored in 64 bits tuples as below: |
| * |
| * (band in source) | (band in target aggregate) |
| */ |
| final var mergedBands = new IdentityHashMap<S,long[]>(); |
| int targetBand = 0; |
| for (int i=0; i<validatedSourceCount; i++) { |
| final int[] selected = bandsPerSource[i]; |
| final long[] tuples = new long[selected.length]; |
| for (int j=0; j<selected.length; j++) { |
| tuples[j] = Numerics.tuple(selected[j], targetBand++); |
| } |
| mergedBands.merge(sources[i], tuples, ArraysExt::concatenate); |
| } |
| /* |
| * Iterate again over the sources, rewriting the arrays with consolidated bands. |
| * We need to keep trace of how the bands were reordered. |
| */ |
| final int[] reordered = new int[totalBandCount]; |
| final int count = validatedSourceCount; |
| validatedSourceCount = 0; |
| targetBand = 0; |
| for (int i=0; i<count; i++) { |
| final S source = sources[i]; |
| final long[] tuples = mergedBands.remove(source); |
| if (tuples != null) { |
| int[] selected = bandsPerSource[i]; |
| if (tuples.length > selected.length) { |
| /* |
| * Found a case where the same source appears two ore more times. |
| * Sort the bands in increasing order for making easier to detect |
| * duplicated values, and because it increases the chances to get |
| * an identity selection (bands in same order) for that source. |
| */ |
| Arrays.sort(tuples); |
| selected = new int[tuples.length]; |
| } |
| /* |
| * Rewrite the `selected` array with the potentially merged bands. |
| * If the source was not repeated, `selected` should be unchanged. |
| * But we loop anyway because we also need to write `reordered`. |
| */ |
| for (int j=0; j < tuples.length; j++) { |
| final long t = tuples[j]; |
| reordered[(int) t] = targetBand + j; |
| selected[j] = (int) (t >>> Integer.SIZE); |
| } |
| targetBand += tuples.length; |
| numBandsPerSource[validatedSourceCount] = numBandsPerSource[i]; |
| bandsPerSource[validatedSourceCount] = selected; |
| sources[validatedSourceCount++] = source; |
| } |
| } |
| return reordered; |
| } |
| |
| /** |
| * Returns {@code true} if there is only one source with all bands selected. |
| * |
| * @return whether {@code sources[0]} could be used directly. |
| */ |
| public boolean isIdentity() { |
| checkValidationState(true); |
| return validatedSourceCount == 1 && isIdentity(0); |
| } |
| |
| /** |
| * Returns {@code true} if the band selection at the specified index is an identity operation. |
| * |
| * @param i index of a source. |
| * @return whether band selection for that source is an identity operation. |
| */ |
| private boolean isIdentity(final int i) { |
| final int[] selected = bandsPerSource[i]; |
| return selected.length == numBandsPerSource[i] && ArraysExt.isRange(0, selected); |
| } |
| |
| /** |
| * Returns all sources coverages as a (potentially modified) |
| * copy of the array argument given to the constructor. |
| * |
| * @return all validated sources. |
| */ |
| public S[] sources() { |
| checkValidationState(true); |
| return sources = ArraysExt.resize(sources, validatedSourceCount); |
| } |
| |
| /** |
| * Returns the indices of selected bands as (potentially modified) |
| * copies of the arrays argument given to the constructor. |
| * |
| * @param identityAsNull whether to use {@code null} elements for meaning "all bands". |
| * @return indices of selected sample dimensions for each source. |
| * Never null but may contain null elements if {@code identityAsNull} is {@code true}. |
| */ |
| public int[][] bandsPerSource(final boolean identityAsNull) { |
| checkValidationState(true); |
| bandsPerSource = ArraysExt.resize(bandsPerSource, validatedSourceCount); |
| if (identityAsNull) { |
| for (int i=0; i<validatedSourceCount; i++) { |
| if (isIdentity(i)) { |
| bandsPerSource[i] = null; |
| } |
| } |
| } |
| return bandsPerSource; |
| } |
| |
| /** |
| * Returns the total number of bands. |
| * |
| * @return total number of bands. |
| */ |
| public int numBands() { |
| checkValidationState(true); |
| return totalBandCount; |
| } |
| |
| /** |
| * Returns the union of all selected bands in all specified sources. |
| * The returned list is modifiable. |
| * |
| * @return all selected sample dimensions. |
| */ |
| public List<SampleDimension> ranges() { |
| if (ranges != null) return ranges; |
| throw new IllegalStateException(); |
| } |
| |
| /** |
| * Computes the intersection of the grid geometries of all sources. |
| * This method also verifies that all grid geometries are compatible. |
| * |
| * @param getter the method to invoke for getting grid geometry from a source. |
| * @return intersection of all grid geometries. |
| * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others. |
| */ |
| public GridGeometry domain(final Function<S, GridGeometry> getter) { |
| checkValidationState(true); |
| final var finder = new CommonDomainFinder(PixelInCell.CELL_CORNER); |
| finder.setFromGridAligned(Arrays.stream(sources).map(getter).toArray(GridGeometry[]::new)); |
| sourceOfGridToCRS = finder.sourceOfGridToCRS(); |
| gridTranslations = finder.gridTranslations(); |
| return finder.result(); |
| } |
| |
| /** |
| * Returns the translations in units of grid cells to apply for obtaining a grid geometry |
| * compatible with the "grid to CRS" transform of a source. |
| * |
| * <p>The returned array should not be modified because it is not cloned.</p> |
| * |
| * @return translations from the common grid geometry to all items. This array is not cloned. |
| */ |
| @SuppressWarnings("ReturnOfCollectionOrArrayField") |
| public long[][] gridTranslations() { |
| checkValidationState(true); |
| return gridTranslations; |
| } |
| |
| /** |
| * Returns the index of a source having the same "grid to CRS" transform than the grid geometry |
| * returned by {@link #domain(Function)}. |
| * |
| * @return index of a sources having the same "grid to CRS" than the domain, or -1 if none. |
| */ |
| public int sourceOfGridToCRS() { |
| checkValidationState(true); |
| return sourceOfGridToCRS; |
| } |
| } |