You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/11/25 17:17:29 UTC

(sis) 03/03: 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.

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 <ma...@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();
+    }
+}