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 2b6989782e5def3b42bdf670ea42d1efd672e359 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Nov 25 18:16:17 2023 +0100 Add in `CoverageAggregator` the same convenience methods than the ones added in `GridCoverageProcessor` in previous commit. The intend is to make easier to append a vertical or temporal dimension to two-dimensional coverages to aggregate in a cube. --- .../sis/coverage/grid/GridCoverageProcessor.java | 2 +- .../org/apache/sis/coverage/grid/GridExtent.java | 15 ++ .../sis/storage/aggregate/CoverageAggregator.java | 149 ++++++++++++- .../sis/storage/aggregate/DimensionAppender.java | 244 +++++++++++++++++++++ 4 files changed, 408 insertions(+), 2 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java index d7194127b1..d5ce4ed858 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java @@ -660,7 +660,7 @@ public class GridCoverageProcessor implements Cloneable { * @param source the source on which to append a dimension. * @param lower lower coordinate value of the slice, in units of the CRS. * @param span size of the slice, in units of the CRS. - * @param crs coordinate reference system of the slice, or {@code null} if unknown. + * @param crs one-dimensional coordinate reference system of the slice, or {@code null} if unknown. * @return a coverage with the specified dimension added. * @throws IllegalGridGeometryException if the compound CRS or compound extent cannot be created. * diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java index d996db4595..d58c9da9df 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java @@ -35,6 +35,7 @@ import org.opengis.geometry.MismatchedDimensionException; import org.opengis.metadata.spatial.DimensionNameType; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CoordinateSystem; +import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.Matrix; @@ -406,6 +407,20 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable System.arraycopy(upper.coordinates, d2, coordinates, dim+d1, d2); } + /** + * Suggests a grid dimension name for the given coordinate system axis. + * Note that grid axes are not necessarily in the same order than CRS axes. + * This method may be used when the caller knows which CRS axis will be associated to a grid axis. + * + * @param axis the coordinate system axis for which to get a suggested grid dimension name. + * @return suggested grid dimension for the given CRS axis. + * + * @since 1.5 + */ + public static Optional<DimensionNameType> typeFromAxis(final CoordinateSystemAxis axis) { + return Optional.ofNullable(AXIS_DIRECTIONS.get(AxisDirections.absolute(axis.getDirection()))); + } + /** * Infers the axis types from the given coordinate reference system. * This method is the converse of {@link GridExtentCRS}. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java index f2895a55ec..a3cff71f4d 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java @@ -27,8 +27,17 @@ import java.util.IdentityHashMap; import java.util.Collections; import java.util.Optional; import java.util.stream.Stream; +import java.time.Instant; +import java.time.Duration; import org.opengis.util.GenericName; +import org.opengis.metadata.spatial.DimensionNameType; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.datum.PixelInCell; +import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.NoninvertibleTransformException; +import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.referencing.crs.DefaultTemporalCRS; +import org.apache.sis.referencing.CommonCRS; import org.apache.sis.image.Colorizer; import org.apache.sis.storage.Resource; import org.apache.sis.storage.Aggregate; @@ -36,13 +45,16 @@ import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.base.MemoryGridResource; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.coverage.SubspaceNotSpecifiedException; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.coverage.grid.GridCoverageProcessor; import org.apache.sis.coverage.grid.IllegalGridGeometryException; -import org.apache.sis.storage.base.MemoryGridResource; import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.internal.Numerics; /** @@ -203,6 +215,63 @@ public final class CoverageAggregator extends Group<GroupBySample> { } } + /** + * Adds the given coverage augmented with the specified grid dimensions. + * The {@code dimToAdd} argument contains typically vertical or temporal axes to add to a two-dimensional coverage. + * All additional dimensions in {@code dimToAdd} must have a grid extent size of one cell. + * + * @param coverage coverage to add. + * @param dimToAdd the dimensions to append. The grid extent size must be 1 cell in all dimensions. + * @throws IllegalGridGeometryException if a dimension has more than one grid cell, or concatenation + * would result in duplicated {@linkplain GridExtent#getAxisType(int) grid axis types}, + * or the compound CRS cannot be created. + * + * @see GridCoverageProcessor#appendDimensions(GridCoverage, GridGeometry) + * + * @since 1.5 + */ + public void add(GridCoverage coverage, GridGeometry dimToAdd) { + add(processor().appendDimensions(coverage, dimToAdd)); + } + + /** + * Adds the given coverage augmented with a single grid dimension. + * The additional dimension is typically a vertical axis to add to a two-dimensional coverage. + * + * @param coverage coverage to add. + * @param lower lower coordinate value of the slice, in units of the CRS. + * @param span size of the slice, in units of the CRS. + * @param crs one-dimensional coordinate reference system of the slice, or {@code null} if unknown. + * @throws IllegalGridGeometryException if the compound CRS or compound extent cannot be created. + * + * @see GridCoverageProcessor#appendDimension(GridCoverage, double, double, CoordinateReferenceSystem) + * + * @since 1.5 + */ + public void add(GridCoverage coverage, double lower, double span, CoordinateReferenceSystem crs) { + add(processor().appendDimension(coverage, lower, span, crs)); + } + + /** + * Adds the given coverage augmented with a single temporal grid dimension. + * This method is provided for convenience, but should be used carefully. + * Slice coordinates computed from calendars tend to produce slices at irregular intervals + * or with heterogeneous spans, which result in coverages that cannot be aggregated by this + * {@code CoverageAggregator} class. + * + * @param source the source on which to append a temporal dimension. + * @param lower start time of the slice. + * @param span duration of the slice. + * @throws IllegalGridGeometryException if the compound CRS or compound extent cannot be created. + * + * @see GridCoverageProcessor#appendDimension(GridCoverage, Instant, Duration) + * + * @since 1.5 + */ + public void add(GridCoverage coverage, Instant lower, Duration span) { + add(processor().appendDimension(coverage, lower, span)); + } + /** * Adds the given resource. This method can be invoked from any thread. * This method does <em>not</em> recursively decomposes an {@link Aggregate} into its component. @@ -225,6 +294,84 @@ public final class CoverageAggregator extends Group<GroupBySample> { } } + /** + * Adds the given resource augmented with the specified grid dimensions. + * The {@code dimToAdd} argument contains typically vertical or temporal axes to add to a two-dimensional resource. + * All additional dimensions in {@code dimToAdd} must have a grid extent size of one cell. + * + * @param resource resource to add. + * @param dimToAdd the dimensions to append. The grid extent size must be 1 cell in all dimensions. + * @throws IllegalGridGeometryException if the compound CRS or compound extent cannot be created. + * @throws DataStoreException if the resource cannot be used. + * + * @since 1.5 + */ + public void add(GridCoverageResource resource, GridGeometry dimToAdd) throws DataStoreException { + add(DimensionAppender.create(processor(), resource, dimToAdd)); + } + + /** + * Adds the given resource augmented with a single grid dimension. + * The additional dimension is typically a vertical axis to add to a two-dimensional resource. + * + * @param resource resource to add. + * @param lower lower coordinate value of the slice, in units of the CRS. + * @param span size of the slice, in units of the CRS. + * @param crs one-dimensional coordinate reference system of the slice, or {@code null} if unknown. + * @throws IllegalGridGeometryException if the compound CRS or compound extent cannot be created. + * @throws DataStoreException if the resource cannot be used. + * + * @since 1.5 + */ + public void add(final GridCoverageResource resource, final double lower, final double span, + final CoordinateReferenceSystem crs) throws DataStoreException + { + /* + * This code currently duplicates `GridCoverageProcessor.appendDimension(..., double, double, CRS)`, + * but a future version may use the state of this `CoverageAggregator`, for example making a better + * effort to align the resources on the same "gridToCRS" transform. + */ + final long index = Numerics.roundAndClamp(lower / span); + final var indices = new long[] {index}; + final var names = new DimensionNameType[] { + GridExtent.typeFromAxis(crs.getCoordinateSystem().getAxis(0)).orElse(null) + }; + final GridExtent extent = new GridExtent(names, indices, indices, true); + final MathTransform gridToCRS = MathTransforms.linear(span, Math.fma(index, -span, lower)); + add(resource, new GridGeometry(extent, PixelInCell.CELL_CORNER, gridToCRS, crs)); + } + + /** + * Adds the given resource augmented with a single temporal grid dimension. + * This method is provided for convenience, but should be used carefully. + * Slice coordinates computed from calendars tend to produce slices at irregular intervals + * or with heterogeneous spans, which result in coverages that cannot be aggregated by this + * {@code CoverageAggregator} class. + * + * @param resource resource to add. + * @param lower start time of the slice. + * @param span duration of the slice. + * @throws IllegalGridGeometryException if the compound CRS or compound extent cannot be created. + * @throws DataStoreException if the resource cannot be used. + * + * @since 1.5 + */ + public void add(final GridCoverageResource resource, final Instant lower, final Duration span) throws DataStoreException { + /* + * This code currently duplicates `GridCoverageProcessor.appendDimension(..., double, double, CRS)`, + * but a future version may use the state of this `CoverageAggregator`, for example making a better + * effort to align the resources on the same "gridToCRS" transform. + */ + final DefaultTemporalCRS crs = DefaultTemporalCRS.castOrCopy(CommonCRS.Temporal.TRUNCATED_JULIAN.crs()); + double scale = crs.toValue(span); + double offset = crs.toValue(lower); + long index = Numerics.roundAndClamp(offset / scale); // See comment in above method. + offset = crs.toValue(lower.minus(span.multipliedBy(index))); + final GridExtent extent = new GridExtent(DimensionNameType.TIME, index, index, true); + final MathTransform gridToCRS = MathTransforms.linear(scale, offset); + add(resource, new GridGeometry(extent, PixelInCell.CELL_CORNER, gridToCRS, crs)); + } + /** * Adds all components of the given aggregate. This method can be invoked from any thread. * It delegates to {@link #add(GridCoverageResource)} for each component in the aggregate diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionAppender.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionAppender.java new file mode 100644 index 0000000000..d25ac1af63 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionAppender.java @@ -0,0 +1,244 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.Optional; +import org.opengis.util.GenericName; +import org.opengis.util.FactoryException; +import org.opengis.metadata.Metadata; +import org.opengis.referencing.datum.PixelInCell; +import org.opengis.referencing.operation.TransformException; +import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.coverage.grid.GridCoverageProcessor; +import org.apache.sis.coverage.grid.IllegalGridGeometryException; +import org.apache.sis.storage.Query; +import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.RasterLoadingStrategy; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStoreReferencingException; +import org.apache.sis.storage.base.StoreUtilities; +import org.apache.sis.storage.event.StoreEvent; +import org.apache.sis.storage.event.StoreListener; +import org.apache.sis.storage.internal.Resources; +import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.logging.Logging; + + +/** + * A wrapper over an existing grid coverage resource with dimensions appended. + * This wrapper delegates the work to {@link GridCoverageProcessor} after a coverage has been read. + * + * @author Martin Desruisseaux (Geomatys) + */ +final class DimensionAppender implements GridCoverageResource { + /** + * The grid coverage processor to use for creating grid coverages with extra dimensions. + */ + private final GridCoverageProcessor processor; + + /** + * The source grid coverage resource for which to append extra dimensions. + */ + private final GridCoverageResource source; + + /** + * The dimensions added to the source grid coverage. + * Should have a grid size of one cell in all dimensions. + */ + private final GridGeometry dimToAdd; + + /** + * The grid geometry with dimensions appended. + * Created when first requested. + * + * @see #getGridGeometry() + */ + private volatile GridGeometry gridGeometry; + + /** + * Creates a new dimension appender for the given grid coverage resource. + * This constructor does not verify the grid geometry validity. + * It is caller's responsibility to verify that the size is 1 cell. + * + * @param processor the grid coverage processor to use for creating grid coverages with extra dimensions. + * @param source the source grid coverage for which to append extra dimensions. + * @param dimToAdd the dimensions to add to the source grid coverage. + */ + private DimensionAppender(final GridCoverageProcessor processor, final GridCoverageResource source, final GridGeometry dimToAdd) { + this.processor = processor; + this.source = source; + this.dimToAdd = dimToAdd; + } + + /** + * Creates a grid coverage resource augmented with the given dimensions. + * The grid extent of {@code dimToAdd} shall have a grid size of one cell in all dimensions. + * + * @param processor the grid coverage processor to use for creating grid coverages with extra dimensions. + * @param source the source grid coverage for which to append extra dimensions. + * @param dimToAdd the dimensions to add to the source grid coverage. + * @throws IllegalGridGeometryException if a dimension has more than one grid cell. + */ + static GridCoverageResource create(final GridCoverageProcessor processor, GridCoverageResource source, GridGeometry dimToAdd) { + ArgumentChecks.ensureNonNull("source", source); + final GridExtent extent = dimToAdd.getExtent(); + int i = extent.getDimension(); + if (i == 0) { + return source; + } + do { + final long size = extent.getSize(--i); + if (size != 1) { + Object name = extent.getAxisType(i).orElse(null); + if (name == null) name = i; + throw new IllegalGridGeometryException(Resources.format(Resources.Keys.NoSliceSpecified_2, name, size)); + } + } while (i != 0); + if (source instanceof DimensionAppender) try { + final var a = (DimensionAppender) source; + dimToAdd = new GridGeometry(a.dimToAdd, dimToAdd); + source = a.source; + } catch (FactoryException e) { + throw new IllegalGridGeometryException(e.getMessage(), e); + } + return new DimensionAppender(processor, source, dimToAdd); + } + + /** + * Returns the identifier of the original resource. + */ + @Override + public Optional<GenericName> getIdentifier() throws DataStoreException { + return source.getIdentifier(); + } + + /** + * Returns the metadata of the original resource. + */ + @Override + public Metadata getMetadata() throws DataStoreException { + return source.getMetadata(); + } + + /** + * Returns the sample dimensions of the original resource. + * Those dimensions are not impacted by the change of domain dimensions. + */ + @Override + public List<SampleDimension> getSampleDimensions() throws DataStoreException { + return source.getSampleDimensions(); + } + + /** + * Returns the grid geometry of the original resource augmented with the dimensions to append. + */ + @Override + public GridGeometry getGridGeometry() throws DataStoreException { + GridGeometry gg = gridGeometry; + if (gg == null) try { + gg = new GridGeometry(source.getGridGeometry(), dimToAdd); + gridGeometry = gg; + } catch (FactoryException | RuntimeException e) { + throw new DataStoreReferencingException(e.getMessage(), e); + } + return gg; + } + + /** + * Returns a subset of this grid coverage resource. + * The result will have the same "dimensions to add" than this resource. + * + * @param query the query to execute. + * @return subset of this coverage resource. + */ + @Override + public GridCoverageResource subset(final Query query) throws DataStoreException { + final GridCoverageResource subset = source.subset(query); + if (subset == source) return this; + return new DimensionAppender(processor, subset, dimToAdd); + } + + /** + * Reads the data and wraps the result with the dimensions to add. + */ + @Override + public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException { + return processor.appendDimensions(source.read(domain, ranges), dimToAdd); + } + + /** + * Returns an indication about when the "physical" loading of raster data will happen. + */ + @Override + public RasterLoadingStrategy getLoadingStrategy() throws DataStoreException { + return source.getLoadingStrategy(); + } + + /** + * Sets the preferred strategy about when to do the "physical" loading of raster data. + */ + @Override + public boolean setLoadingStrategy(RasterLoadingStrategy strategy) throws DataStoreException { + return source.setLoadingStrategy(strategy); + } + + /** + * Registers a listener to notify when the specified kind of event occurs in this resource or in children. + */ + @Override + public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) { + source.addListener(eventType, listener); + } + + /** + * Unregisters a listener previously added to this resource for the given type of events. + */ + @Override + public <T extends StoreEvent> void removeListener(Class<T> eventType, StoreListener<? super T> listener) { + source.removeListener(eventType, listener); + } + + /** + * Returns a string representation of this wrapper for debugging purposes. + */ + @Override + public String toString() { + final var sb = new StringBuilder(40); + sb.append(source).append(" + dimensions["); + final GridExtent extent = dimToAdd.getExtent(); + final double[] coordinates = ArraysExt.copyAsDoubles(extent.getLow().getCoordinateValues()); + try { + dimToAdd.getGridToCRS(PixelInCell.CELL_CORNER).transform(coordinates, 0, coordinates, 0, 1); + } catch (RuntimeException | TransformException e) { + // Should never happen because the transform should be linear. + Logging.unexpectedException(StoreUtilities.LOGGER, DimensionAppender.class, "toString", e); + Arrays.fill(coordinates, Double.NaN); + } + for (int i=0; i<coordinates.length; i++) { + if (i != 0) sb.append(", "); + extent.getAxisType(i).ifPresent((type) -> sb.append(type.name()).append('=')); + sb.append(coordinates[i]); + } + return sb.append(']').toString(); + } +}