| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.storage.aggregate; |
| |
| import java.util.List; |
| import java.util.Arrays; |
| import java.util.Objects; |
| import java.util.Optional; |
| import org.opengis.util.GenericName; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.metadata.Metadata; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.coverage.SampleDimension; |
| import org.apache.sis.coverage.grid.GridCoverage; |
| import org.apache.sis.coverage.grid.GridDerivation; |
| 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.geometry.ImmutableEnvelope; |
| import org.apache.sis.storage.Resource; |
| import org.apache.sis.storage.AbstractGridCoverageResource; |
| import org.apache.sis.storage.DataStoreException; |
| import org.apache.sis.storage.GridCoverageResource; |
| import org.apache.sis.storage.RasterLoadingStrategy; |
| import org.apache.sis.storage.event.StoreListeners; |
| import org.apache.sis.storage.base.MemoryGridResource; |
| import org.apache.sis.storage.base.MetadataBuilder; |
| import org.apache.sis.coverage.privy.RangeArgument; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.privy.UnmodifiableArrayList; |
| import org.apache.sis.util.privy.Numerics; |
| import org.apache.sis.pending.jdk.JDK18; |
| |
| |
| /** |
| * A grid coverage resource where a single dimension is the concatenation of many grid coverage resources. |
| * All components must have the same "grid to CRS" transform. |
| * Instances of {@code ConcatenatedGridResource} are created by {@link CoverageAggregator}. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| final class ConcatenatedGridResource extends AbstractGridCoverageResource implements AggregatedResource { |
| /** |
| * The identifier for this aggregate, or {@code null} if none. |
| * This is optionally supplied by users for their own purposes. |
| * There is no default value. |
| */ |
| private GenericName identifier; |
| |
| /** |
| * Name of this resource to use in metadata. |
| */ |
| private String name; |
| |
| /** |
| * The grid geometry of this aggregated resource. |
| * |
| * @see #getGridGeometry() |
| */ |
| private final GridGeometry gridGeometry; |
| |
| /** |
| * The ranges of sample values of this aggregated resource. |
| * Shall be an unmodifiable list. |
| * |
| * @see #getSampleDimensions() |
| */ |
| private final List<SampleDimension> sampleDimensions; |
| |
| /** |
| * Whether all {@link SampleDimension} represent "real world" values. |
| */ |
| final boolean isConverted; |
| |
| /** |
| * The slices of this resource, in the same order as {@link GridSliceLocator#sliceLows}. |
| * Each slice is not necessarily 1 cell tick; larger slices are accepted. |
| * This array shall be read-only. |
| */ |
| private final GridCoverageResource[] slices; |
| |
| /** |
| * Whether loading of grid coverages should be deferred to rendering time. |
| * This is a bit set packed as {@code int} values. A bit value of 1 means |
| * that the coverages at the corresponding index should be loaded from the |
| * {@linkplain #slices} at same index only when first needed. |
| * |
| * <p>Whether a bit is set or not depends on two factor:</p> |
| * <ul> |
| * <li>Whether deferred loading has been requested by a call to {@link #setLoadingStrategy(RasterLoadingStrategy)}.</li> |
| * <li>Whether the slice at the corresponding index can handle deferred loading itself. |
| * In such case, we let the resource manages its own lazy loading.</li> |
| * </ul> |
| * |
| * This array shall be read-only. If changes are desired, a new array shall be created (copy-on-write). |
| * |
| * @see #isDeferred(int) |
| * @see RasterLoadingStrategy#AT_READ_TIME |
| * @see RasterLoadingStrategy#AT_RENDER_TIME |
| */ |
| private int[] deferredLoading; |
| |
| /** |
| * The object for identifying indices in the {@link #slices} array. |
| */ |
| final GridSliceLocator locator; |
| |
| /** |
| * Algorithm to apply when more than one grid coverage can be found at the same grid index. |
| * This is {@code null} if no merge should be attempted. |
| */ |
| final MergeStrategy strategy; |
| |
| /** |
| * The envelope of this aggregate, or {@code null} if not yet computed. |
| * May also be {@code null} if no slice declare an envelope, or if the union cannot be computed. |
| * |
| * @see #getEnvelope() |
| */ |
| private ImmutableEnvelope envelope; |
| |
| /** |
| * Whether {@link #envelope} has been initialized. |
| * The envelope may still be null if the initialization failed. |
| */ |
| private boolean envelopeIsEvaluated; |
| |
| /** |
| * The resolutions, or {@code null} if not yet computed. Can be an empty array after computation. |
| * Shall be read-only after computation. |
| * |
| * @see #getResolutions() |
| */ |
| private double[][] resolutions; |
| |
| /** |
| * Creates a new aggregated resource. |
| * |
| * @param name name of the grid coverage to create. |
| * @param listeners listeners of the parent resource, or {@code null} if none. |
| * @param domain value to be returned by {@link #getGridGeometry()}. |
| * @param ranges value to be returned by {@link #getSampleDimensions()}. |
| * @param slices the slices of this resource, in the same order as {@link GridSliceLocator#sliceLows}. |
| */ |
| ConcatenatedGridResource(final String name, |
| final StoreListeners listeners, |
| final GridGeometry domain, |
| final List<SampleDimension> ranges, |
| final GridCoverageResource[] slices, |
| final GridSliceLocator locator, |
| final MergeStrategy strategy) |
| { |
| super(listeners, false); |
| this.name = name; |
| this.gridGeometry = domain; |
| this.sampleDimensions = ranges; |
| this.slices = slices; |
| this.locator = locator; |
| this.strategy = strategy; |
| this.deferredLoading = new int[JDK18.ceilDiv(slices.length, Integer.SIZE)]; |
| for (final SampleDimension sd : ranges) { |
| if (sd.forConvertedValues(true) != sd) { |
| isConverted = false; |
| return; |
| } |
| } |
| isConverted = true; |
| } |
| |
| /** |
| * Creates a new resource with the same data as given resource but a different merge strategy. |
| * The two resources will share the same cache of loaded coverages. |
| * |
| * @param source the resource to copy. |
| * @param strategy the new merge strategy. |
| */ |
| private ConcatenatedGridResource(final ConcatenatedGridResource source, final MergeStrategy strategy) { |
| super(source.listeners, true); |
| name = source.name; |
| gridGeometry = source.gridGeometry; |
| sampleDimensions = source.sampleDimensions; |
| isConverted = source.isConverted; |
| slices = source.slices; |
| deferredLoading = source.deferredLoading; |
| locator = source.locator; |
| envelope = source.envelope; |
| envelopeIsEvaluated = source.envelopeIsEvaluated; |
| resolutions = source.resolutions; |
| this.strategy = strategy; |
| } |
| |
| /** |
| * Returns a coverage with the same data as this coverage but a different merge strategy. |
| * This is the implementation of {@link MergeStrategy#apply(Resource)} public method. |
| */ |
| @Override |
| public final synchronized Resource apply(final MergeStrategy s) { |
| if (Objects.equals(strategy, s)) return this; |
| return new ConcatenatedGridResource(this, s); |
| } |
| |
| /** |
| * Modifies the name of the resource. |
| * This information is used for metadata. |
| */ |
| @Override |
| public void setName(final String name) { |
| this.name = name; |
| } |
| |
| /** |
| * Sets the identifier of this resource. |
| */ |
| @Override |
| public void setIdentifier(final GenericName identifier) { |
| this.identifier = identifier; |
| } |
| |
| |
| /** |
| * Returns the resource persistent identifier as specified by the |
| * user in {@link CoverageAggregator}. There is no default value. |
| */ |
| @Override |
| public Optional<GenericName> getIdentifier() { |
| return Optional.ofNullable(identifier); |
| } |
| |
| /** |
| * Creates when first requested the metadata about this resource. |
| */ |
| @Override |
| protected Metadata createMetadata() throws DataStoreException { |
| final MetadataBuilder builder = new MetadataBuilder(); |
| builder.addTitle(name); |
| builder.addDefaultMetadata(this, listeners); |
| return builder.build(); |
| } |
| |
| /** |
| * Returns the grid geometry of this aggregated resource. |
| */ |
| @Override |
| public final GridGeometry getGridGeometry() { |
| return gridGeometry; |
| } |
| |
| /** |
| * Returns the ranges of sample values of this aggregated resource. |
| */ |
| @Override |
| @SuppressWarnings("ReturnOfCollectionOrArrayField") |
| public final List<SampleDimension> getSampleDimensions() { |
| return sampleDimensions; |
| } |
| |
| /** |
| * Returns the spatiotemporal envelope of this resource. |
| * |
| * @return the spatiotemporal resource extent. |
| * @throws DataStoreException if an error occurred while reading or computing the envelope. |
| */ |
| @Override |
| public synchronized Optional<Envelope> getEnvelope() throws DataStoreException { |
| if (!envelopeIsEvaluated) { |
| try { |
| envelope = GroupAggregate.unionOfComponents(slices); |
| } catch (TransformException e) { |
| listeners.warning(e); |
| } |
| envelopeIsEvaluated = true; |
| } |
| return Optional.ofNullable(envelope); |
| } |
| |
| /** |
| * Returns the preferred resolutions (in units of CRS axes) for read operations in this resource. |
| * This method returns only the resolutions that are declared by all coverages. |
| * |
| * @return preferred resolutions for read operations in this resource, or an empty array if none. |
| * @throws DataStoreException if an error occurred while reading definitions from an underlying resource. |
| */ |
| @Override |
| public synchronized List<double[]> getResolutions() throws DataStoreException { |
| if (resolutions == null) { |
| resolutions = commonResolutions(slices); |
| } |
| return UnmodifiableArrayList.wrap(resolutions); |
| } |
| |
| /** |
| * Computes resolutions common to all sources. |
| * |
| * @param sources the sources to use for computing the common resolution. |
| * @return the resolutions common to all given sources. |
| */ |
| static double[][] commonResolutions(final GridCoverageResource[] sources) throws DataStoreException { |
| int count = 0; |
| double[][] resolutions = null; |
| for (final GridCoverageResource slice : sources) { |
| final double[][] sr = slice.getResolutions().toArray(double[][]::new); |
| if (sr != null) { // Should never be null, but we are paranoiac. |
| if (resolutions == null) { |
| resolutions = sr; |
| count = sr.length; |
| } else { |
| int retained = 0; |
| for (int i=0; i<count; i++) { |
| final double[] r = resolutions[i]; |
| for (int j=0; j<sr.length; j++) { |
| if (Arrays.equals(r, sr[j])) { |
| resolutions[retained++] = r; |
| sr[j] = null; |
| break; |
| } |
| } |
| } |
| count = retained; |
| if (count == 0) break; |
| } |
| } |
| } |
| return ArraysExt.resize(resolutions, count); |
| } |
| |
| /** |
| * Returns an indication about when the "physical" loading of raster data will happen. |
| * This method returns the most conservative value of all slices. |
| * |
| * @return current raster data loading strategy for this resource. |
| * @throws DataStoreException if an error occurred while fetching data store configuration. |
| */ |
| @Override |
| public synchronized RasterLoadingStrategy getLoadingStrategy() throws DataStoreException { |
| /* |
| * If at least one bit of `deferredLoading` is set, then it means that |
| * `setLoadingStrategy(…)` has been invoked with anything else than `AT_READ_TIME`. |
| */ |
| RasterLoadingStrategy conservative = RasterLoadingStrategy.AT_GET_TILE_TIME; |
| for (int i=0; i < slices.length; i++) { |
| final GridCoverageResource slice = slices[i]; |
| RasterLoadingStrategy s = slice.getLoadingStrategy(); |
| if (s == null || s.ordinal() == 0) { // Should never be null, but we are paranoiac. |
| s = isDeferred(i) ? RasterLoadingStrategy.AT_RENDER_TIME |
| : RasterLoadingStrategy.AT_READ_TIME; |
| } |
| if (s.ordinal() < conservative.ordinal()) { |
| conservative = s; |
| if (s.ordinal() == 0) break; |
| } |
| } |
| return conservative; |
| } |
| |
| /** |
| * Sets the preferred strategy about when to do the "physical" loading of raster data. |
| * Slices are free to replace the given strategy by another one. |
| * |
| * @param strategy the desired strategy for loading raster data. |
| * @return {@code true} if the given strategy has been accepted by all slices. |
| * @throws DataStoreException if an error occurred while setting data store configuration. |
| */ |
| @Override |
| public synchronized boolean setLoadingStrategy(final RasterLoadingStrategy strategy) throws DataStoreException { |
| final boolean deferred = (strategy.ordinal() != 0); |
| final int[] newValues = new int[deferredLoading.length]; |
| boolean accepted = true; |
| for (int i=0; i < slices.length; i++) { |
| final GridCoverageResource slice = slices[i]; |
| if (!slice.setLoadingStrategy(strategy)) { |
| if (deferred) { |
| newValues[i >>> Numerics.INT_SHIFT] |= (1 << i); |
| } |
| accepted = false; |
| } |
| } |
| if (!Arrays.equals(deferredLoading, newValues)) { |
| deferredLoading = newValues; |
| } |
| return accepted; |
| } |
| |
| /** |
| * Returns {@code true} if the loading of the coverage at the given index is deferred. |
| */ |
| private boolean isDeferred(final int i) { |
| return (deferredLoading[i >>> Numerics.INT_SHIFT] & (1 << i)) != 0; |
| } |
| |
| /** |
| * Loads a subset of the grid coverage represented by this resource. |
| * |
| * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. |
| * @param ranges 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all. |
| * @return the grid coverage for the specified domain and ranges. |
| * @throws DataStoreException if an error occurred while reading the grid coverage data. |
| */ |
| @Override |
| public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException { |
| /* |
| * Validate arguments. |
| */ |
| if (ranges != null) { |
| ranges = RangeArgument.validate(sampleDimensions.size(), ranges, listeners).getSelectedBands(); |
| } |
| int lower = 0, upper = slices.length; |
| if (domain != null) { |
| final GridDerivation subgrid = gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain); |
| domain = subgrid.build(); |
| final GridExtent sliceExtent = subgrid.getIntersection(); |
| upper = locator.getUpper(sliceExtent, lower, upper); |
| lower = locator.getLower(sliceExtent, lower, upper); |
| } |
| /* |
| * At this point we got the indices of the slices to read. The range should not be empty (upper > lower). |
| * Create arrays with only the requested range, without keeping reference to this concatenated resource, |
| * for allowing garbage-collection of resources outside that range. |
| */ |
| final int count = upper - lower; |
| final Object[] coverages = new Object[count]; |
| final GridGeometry[] geometries = new GridGeometry[count]; |
| int[] deferred = null; |
| for (int i=0; i<count; i++) { |
| final int j = lower + i; |
| final GridCoverageResource slice = slices[j]; |
| if (slice instanceof MemoryGridResource) { |
| final GridCoverage coverage = ((MemoryGridResource) slice).coverage; |
| coverages [i] = coverage; |
| geometries[i] = coverage.getGridGeometry(); |
| } else if (!isDeferred(j) || count <= 1) { |
| final GridCoverage coverage = slice.read(domain, ranges); |
| coverages [i] = coverage; |
| geometries[i] = coverage.getGridGeometry(); |
| } else { |
| coverages [i] = slice; |
| geometries[i] = slice.getGridGeometry(); |
| if (deferred == null) { |
| deferred = new int[JDK18.ceilDiv(count, Integer.SIZE)]; |
| } |
| deferred[i >>> Numerics.INT_SHIFT] |= (1 << i); |
| } |
| } |
| /* |
| * Following cast should never fail because the `count <= 1` check in above |
| * loop ensured that we loaded the coverage. |
| * Otherwise (if more than one slice), create a concatenation of all slices. |
| */ |
| if (count == 1) { |
| return (GridCoverage) coverages[0]; |
| } |
| if (Arrays.equals(deferred, deferredLoading)) { |
| deferred = deferredLoading; // Slight memory saving for a common case. |
| } |
| final GridGeometry union = locator.union(gridGeometry, Arrays.asList(geometries), GridGeometry::getExtent); |
| return new ConcatenatedGridCoverage(this, union, coverages, lower, deferred, domain, ranges); |
| } |
| } |